🎖️GitЯра🎖️
Commit 953eef941e7b5e5335f2724cffd2dc1e4ad33107
Parents : a09e136
Author : James Rich <2199651+jamesarich@users.noreply.github.com>
Date : 2026-06-10T16:25:27-05:00
refactor(node): fetch device links from the API, drop the bundled matcher (#5765)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Changes
24 files changed, 5648 insertions(+), 1706 deletions(-)
Diff
diff --git a/androidApp/src/fdroid/kotlin/org/meshtastic/app/di/FDroidNetworkModule.kt b/androidApp/src/fdroid/kotlin/org/meshtastic/app/di/FDroidNetworkModule.kt
index ca81dbada9..4cdaabc0b2 100644
--- a/androidApp/src/fdroid/kotlin/org/meshtastic/app/di/FDroidNetworkModule.kt
+++ b/androidApp/src/fdroid/kotlin/org/meshtastic/app/di/FDroidNetworkModule.kt
@@ -19,6 +19,7 @@ package org.meshtastic.app.di
import org.koin.core.annotation.Module
import org.koin.core.annotation.Single
import org.meshtastic.core.model.NetworkDeviceHardware
+import org.meshtastic.core.model.NetworkDeviceLinksResponse
import org.meshtastic.core.model.NetworkFirmwareReleases
import org.meshtastic.core.network.service.ApiService
@@ -36,6 +37,9 @@ class FDroidNetworkModule {
override suspend fun getDeviceHardware(): List<NetworkDeviceHardware> =
throw UnsupportedOperationException("getDeviceHardware is not supported on F-Droid builds.")
+ override suspend fun getDeviceLinks(): NetworkDeviceLinksResponse =
+ throw UnsupportedOperationException("getDeviceLinks is not supported on F-Droid builds.")
+
override suspend fun getFirmwareReleases(): NetworkFirmwareReleases =
throw UnsupportedOperationException("getFirmwareReleases is not supported on F-Droid builds.")
}
diff --git a/androidApp/src/main/assets/device_links.json b/androidApp/src/main/assets/device_links.json
new file mode 100644
index 0000000000..38e73f4a5d
--- /dev/null
+++ b/androidApp/src/main/assets/device_links.json
@@ -0,0 +1,3646 @@
+{
+ "version": 1,
+ "generatedAt": "2026-06-10T14:17:34.591Z",
+ "source": "https://msh.to/api/urls",
+ "links": [
+ {
+ "shortCode": "github",
+ "url": "https://msh.to/github",
+ "description": "Meshtastic GitHub Organization",
+ "type": "internal",
+ "targets": null,
+ "hwModels": null,
+ "marketplace": null,
+ "regions": null
+ },
+ {
+ "shortCode": "youtube",
+ "url": "https://msh.to/youtube",
+ "description": "Meshtastic YouTube Channel",
+ "type": "internal",
+ "targets": null,
+ "hwModels": null,
+ "marketplace": null,
+ "regions": null
+ },
+ {
+ "shortCode": "reddit",
+ "url": "https://msh.to/reddit",
+ "description": "Meshtastic Reddit Community",
+ "type": "internal",
+ "targets": null,
+ "hwModels": null,
+ "marketplace": null,
+ "regions": null
+ },
+ {
+ "shortCode": "docs",
+ "url": "https://msh.to/docs",
+ "description": "Meshtastic Documentation",
+ "type": "internal",
+ "targets": null,
+ "hwModels": null,
+ "marketplace": null,
+ "regions": null
+ },
+ {
+ "shortCode": "discord",
+ "url": "https://msh.to/discord",
+ "description": "Meshtastic Discord Server",
+ "type": "internal",
+ "targets": null,
+ "hwModels": null,
+ "marketplace": null,
+ "regions": null
+ },
+ {
+ "shortCode": "web",
+ "url": "https://msh.to/web",
+ "description": "Meshtastic Web Client",
+ "type": "internal",
+ "targets": null,
+ "hwModels": null,
+ "marketplace": null,
+ "regions": null
+ },
+ {
+ "shortCode": "flash",
+ "url": "https://msh.to/flash",
+ "description": "Meshtastic Web Flasher",
+ "type": "internal",
+ "targets": null,
+ "hwModels": null,
+ "marketplace": null,
+ "regions": null
+ },
+ {
+ "shortCode": "firmware",
+ "url": "https://msh.to/firmware",
+ "description": "Meshtastic Firmware Repository",
+ "type": "internal",
+ "targets": null,
+ "hwModels": null,
+ "marketplace": null,
+ "regions": null
+ },
+ {
+ "shortCode": "android",
+ "url": "https://msh.to/android",
+ "description": "Meshtastic Android App",
+ "type": "internal",
+ "targets": null,
+ "hwModels": null,
+ "marketplace": null,
+ "regions": null
+ },
+ {
+ "shortCode": "ios",
+ "url": "https://msh.to/ios",
+ "description": "Meshtastic iOS App",
+ "type": "internal",
+ "targets": null,
+ "hwModels": null,
+ "marketplace": null,
+ "regions": null
+ },
+ {
+ "shortCode": "rak-collection",
+ "url": "https://msh.to/rak-collection",
+ "description": "RAKwireless Meshtastic Collection",
+ "type": "vendor",
+ "targets": [],
+ "hwModels": [],
+ "marketplace": null,
+ "regions": null
+ },
+ {
+ "shortCode": "rak4631",
+ "url": "https://msh.to/rak4631",
+ "description": "WisMesh RAK4631 Starter Kit",
+ "type": "vendor",
+ "targets": [
+ "rak4631"
+ ],
+ "hwModels": [
+ 9
+ ],
+ "marketplace": null,
+ "regions": null
+ },
+ {
+ "shortCode": "rak3312",
+ "url": "https://msh.to/rak3312",
+ "description": "WisMesh ESP32-S3 Starter Kit",
+ "type": "vendor",
+ "targets": [
+ "rak3312"
+ ],
+ "hwModels": [
+ 106
+ ],
+ "marketplace": null,
+ "regions": null
+ },
+ {
+ "shortCode": "rak3401-1watt",
+ "url": "https://msh.to/rak3401-1watt",
+ "description": "WisMesh RAK3401 1W Starter Kit",
+ "type": "vendor",
+ "targets": [
+ "rak3401-1watt"
+ ],
+ "hwModels": [
+ 117
+ ],
+ "marketplace": null,
+ "regions": null
+ },
+ {
+ "shortCode": "rak_wismeshtap",
+ "url": "https://msh.to/rak_wismeshtap",
+ "description": "RAK WisMesh Tap",
+ "type": "vendor",
+ "targets": [
+ "rak_wismeshtap"
+ ],
+ "hwModels": [
+ 84
+ ],
+ "marketplace": null,
+ "regions": null
+ },
+ {
+ "shortCode": "rak_wismeshtag",
+ "url": "https://msh.to/rak_wismeshtag",
+ "description": "RAK WisMesh Tag",
+ "type": "vendor",
+ "targets": [
+ "rak_wismeshtag"
+ ],
+ "hwModels": [
+ 105
+ ],
+ "marketplace": null,
+ "regions": null
+ },
+ {
+ "shortCode": "rokland-wismesh-tag",
+ "url": "https://msh.to/rokland-wismesh-tag",
+ "description": "Rokland WisMesh Tag",
+ "type": "marketplace",
+ "targets": [
+ "rak_wismeshtag"
+ ],
+ "hwModels": [
+ 105
+ ],
+ "marketplace": "rokland",
+ "regions": [
+ "AU",
+ "AT",
+ "BE",
+ "CA",
+ "DK",
+ "EC",
+ "FR",
+ "DE",
+ "IE",
+ "JP",
+ "NL",
+ "NZ",
+ "NO",
+ "PK",
+ "ES",
+ "SE",
+ "CH",
+ "GB",
+ "US"
+ ]
+ },
+ {
+ "shortCode": "hexaspot-wismesh-tag",
+ "url": "https://msh.to/hexaspot-wismesh-tag",
+ "description": "Hexaspot WisMesh Tag",
+ "type": "marketplace",
+ "targets": [
+ "rak_wismeshtag"
+ ],
+ "hwModels": [
+ 105
+ ],
+ "marketplace": "hexaspot",
+ "regions": [
+ "AT",
+ "BE",
+ "BG",
+ "CY",
+ "CZ",
+ "DE",
+ "DK",
+ "EE",
+ "ES",
+ "FI",
+ "FR",
+ "GR",
+ "HR",
+ "HU",
+ "IE",
+ "IT",
+ "LT",
+ "LU",
+ "LV",
+ "MT",
+ "NL",
+ "NO",
+ "PL",
+ "PT",
+ "RO",
+ "SE",
+ "SI",
+ "SK"
+ ]
+ },
+ {
+ "shortCode": "aliexpress-wismesh-tag",
+ "url": "https://msh.to/aliexpress-wismesh-tag",
+ "description": "Aliexpress RAK WisMesh Tag",
+ "type": "marketplace",
+ "targets": [
+ "rak_wismeshtag"
+ ],
+ "hwModels": [
+ 105
+ ],
+ "marketplace": "aliexpress",
+ "regions": null
+ },
+ {
+ "shortCode": "rak19007",
+ "url": "https://msh.to/rak19007",
+ "description": "RAKwireless RAK19007 WisBlock Base Board 2nd Gen",
+ "type": "vendor",
+ "targets": [],
+ "hwModels": [],
+ "marketplace": null,
+ "regions": null
+ },
+ {
+ "shortCode": "tbeam-s3-core",
+ "url": "https://msh.to/tbeam-s3-core",
+ "description": "T-Beam Supreme",
+ "type": "vendor",
+ "targets": [
+ "tbeam-s3-core"
+ ],
+ "hwModels": [
+ 12
+ ],
+ "marketplace": null,
+ "regions": null
+ },
+ {
+ "shortCode": "t-echo",
+ "url": "https://msh.to/t-echo",
+ "description": "T-Echo",
+ "type": "vendor",
+ "targets": [
+ "t-echo"
+ ],
+ "hwModels": [
+ 7
+ ],
+ "marketplace": null,
+ "regions": null
+ },
+ {
+ "shortCode": "t-watch-s3",
+ "url": "https://msh.to/t-watch-s3",
+ "description": "T-Watch S3",
+ "type": "vendor",
+ "targets": [
+ "t-watch-s3"
+ ],
+ "hwModels": [
+ 51
+ ],
+ "marketplace": null,
+ "regions": null
+ },
+ {
+ "shortCode": "t-deck",
+ "url": "https://msh.to/t-deck",
+ "description": "T-Deck",
+ "type": "vendor",
+ "targets": [
+ "t-deck"
+ ],
+ "hwModels": [
+ 50
+ ],
+ "marketplace": null,
+ "regions": null
+ },
+ {
+ "shortCode": "tlora-t3s3-v1",
+ "url": "https://msh.to/tlora-t3s3-v1",
+ "description": "T3S3",
+ "type": "vendor",
+ "targets": [
+ "tlora-t3s3-v1"
+ ],
+ "hwModels": [
+ 16
+ ],
+ "marketplace": null,
+ "regions": null
+ },
+ {
+ "shortCode": "heltec-mesh-node-t114",
+ "url": "https://msh.to/heltec-mesh-node-t114",
+ "description": "Mesh Node T114",
+ "type": "vendor",
+ "targets": [
+ "heltec-mesh-node-t114"
+ ],
+ "hwModels": [
+ 69
+ ],
+ "marketplace": null,
+ "regions": null
+ },
+ {
+ "shortCode": "heltec-vision-master-e213",
+ "url": "https://msh.to/heltec-vision-master-e213",
+ "description": "Vision Master E213",
+ "type": "vendor",
+ "targets": [
+ "heltec-vision-master-e213"
+ ],
+ "hwModels": [
+ 67
+ ],
+ "marketplace": null,
+ "regions": null
+ },
+ {
+ "shortCode": "heltec-vision-master-e290",
+ "url": "https://msh.to/heltec-vision-master-e290",
+ "description": "Vision Master E290",
+ "type": "vendor",
+ "targets": [
+ "heltec-vision-master-e290"
+ ],
+ "hwModels": [
+ 68
+ ],
+ "marketplace": null,
+ "regions": null
+ },
+ {
+ "shortCode": "heltec-vision-master-t190",
+ "url": "https://msh.to/heltec-vision-master-t190",
+ "description": "Vision Master T190",
+ "type": "vendor",
+ "targets": [
+ "heltec-vision-master-t190"
+ ],
+ "hwModels": [
+ 66
+ ],
+ "marketplace": null,
+ "regions": null
+ },
+ {
+ "shortCode": "heltec-wireless-tracker",
+ "url": "https://msh.to/heltec-wireless-tracker",
+ "description": "Wireless Tracker",
+ "type": "vendor",
+ "targets": [
+ "heltec-wireless-tracker"
+ ],
+ "hwModels": [
+ 48
+ ],
+ "marketplace": null,
+ "regions": null
+ },
+ {
+ "shortCode": "heltec-wireless-tracker-v2",
+ "url": "https://msh.to/heltec-wireless-tracker-v2",
+ "description": "Wireless Tracker V2",
+ "type": "vendor",
+ "targets": [
+ "heltec-wireless-tracker-v2"
+ ],
+ "hwModels": [
+ 113
+ ],
+ "marketplace": null,
+ "regions": null
+ },
+ {
+ "shortCode": "heltec-wireless-paper",
+ "url": "https://msh.to/heltec-wireless-paper",
+ "description": "Wireless Paper",
+ "type": "vendor",
+ "targets": [
+ "heltec-wireless-paper"
+ ],
+ "hwModels": [
+ 49
+ ],
+ "marketplace": null,
+ "regions": null
+ },
+ {
+ "shortCode": "heltec-ht62-esp32c3-sx1262",
+ "url": "https://msh.to/heltec-ht62-esp32c3-sx1262",
+ "description": "HT-CT62",
+ "type": "vendor",
+ "targets": [
+ "heltec-ht62-esp32c3-sx1262"
+ ],
+ "hwModels": [
+ 53
+ ],
+ "marketplace": null,
+ "regions": null
+ },
+ {
+ "shortCode": "wio-tracker-wm1110",
+ "url": "https://msh.to/wio-tracker-wm1110",
+ "description": "Wio Tracker WM1110 Dev Kit",
+ "type": "vendor",
+ "targets": [
+ "wio-tracker-wm1110"
+ ],
+ "hwModels": [
+ 21
+ ],
+ "marketplace": null,
+ "regions": null
+ },
+ {
+ "shortCode": "tracker-t1000-e",
+ "url": "https://msh.to/tracker-t1000-e",
+ "description": "SenseCAP Card Tracker T1000-E",
+ "type": "vendor",
+ "targets": [
+ "tracker-t1000-e"
+ ],
+ "hwModels": [
+ 71
+ ],
+ "marketplace": null,
+ "regions": null
+ },
+ {
+ "shortCode": "tracker-t1000-e-aliexpress",
+ "url": "https://msh.to/tracker-t1000-e-aliexpress",
+ "description": "SenseCAP Card Tracker T1000-E Aliexpress",
+ "type": "marketplace",
+ "targets": [
+ "tracker-t1000-e"
+ ],
+ "hwModels": [
+ 71
+ ],
+ "marketplace": "aliexpress",
+ "regions": null
+ },
+ {
+ "shortCode": "tracker-t1000-e-amazon",
+ "url": "https://msh.to/tracker-t1000-e-amazon",
+ "description": "SenseCAP Card Tracker T1000-E Amazon",
+ "type": "marketplace",
+ "targets": [
+ "tracker-t1000-e"
+ ],
+ "hwModels": [
+ 71
+ ],
+ "marketplace": "amazon",
+ "regions": [
+ "AU",
+ "CA",
+ "FR",
+ "DE",
+ "IE",
+ "JP",
+ "NL",
+ "ES",
+ "SE",
+ "GB",
+ "US"
+ ]
+ },
+ {
+ "shortCode": "seeed-sensecap-indicator",
+ "url": "https://msh.to/seeed-sensecap-indicator",
+ "description": "SenseCAP Indicator",
+ "type": "vendor",
+ "targets": [
+ "seeed-sensecap-indicator"
+ ],
+ "hwModels": [
+ 70
+ ],
+ "marketplace": null,
+ "regions": null
+ },
+ {
+ "shortCode": "station-g2",
+ "url": "https://msh.to/station-g2",
+ "description": "Station G2",
+ "type": "vendor",
+ "targets": [
+ "station-g2"
+ ],
+ "hwModels": [
+ 31
+ ],
+ "marketplace": null,
+ "regions": null
+ },
+ {
+ "shortCode": "rak2560",
+ "url": "https://msh.to/rak2560",
+ "description": "WisMesh Repeater",
+ "type": "vendor",
+ "targets": [
+ "rak2560"
+ ],
+ "hwModels": [
+ 22
+ ],
+ "marketplace": null,
+ "regions": null
+ },
+ {
+ "shortCode": "heltec-v3",
+ "url": "https://msh.to/heltec-v3",
+ "description": "LoRa32 V3",
+ "type": "vendor",
+ "targets": [
+ "heltec-v3"
+ ],
+ "hwModels": [
+ 43
+ ],
+ "marketplace": null,
+ "regions": null
+ },
+ {
+ "shortCode": "heltec-wsl-v3",
+ "url": "https://msh.to/heltec-wsl-v3",
+ "description": "WSL V3",
+ "type": "vendor",
+ "targets": [
+ "heltec-wsl-v3"
+ ],
+ "hwModels": [
+ 44
+ ],
+ "marketplace": null,
+ "regions": null
+ },
+ {
+ "shortCode": "heltec-v4",
+ "url": "https://msh.to/heltec-v4",
+ "description": "LoRa32 V4",
+ "type": "vendor",
+ "targets": [
+ "heltec-v4"
+ ],
+ "hwModels": [
+ 110
+ ],
+ "marketplace": null,
+ "regions": null
+ },
+ {
+ "shortCode": "seeed-xiao-s3",
+ "url": "https://msh.to/seeed-xiao-s3",
+ "description": "XIAO ESP32-S3 + Wio-SX1262 Kit",
+ "type": "vendor",
+ "targets": [
+ "seeed-xiao-s3"
+ ],
+ "hwModels": [
+ 81
+ ],
+ "marketplace": null,
+ "regions": null
+ },
+ {
+ "shortCode": "tlora-t3s3-epaper",
+ "url": "https://msh.to/tlora-t3s3-epaper",
+ "description": "T3S3",
+ "type": "vendor",
+ "targets": [
+ "tlora-t3s3-epaper"
+ ],
+ "hwModels": [
+ 16
+ ],
+ "marketplace": null,
+ "regions": null
+ },
+ {
+ "shortCode": "ht-ct62",
+ "url": "https://msh.to/ht-ct62",
+ "description": "HT-CT62",
+ "type": "vendor",
+ "targets": [
+ "heltec-ht62-esp32c3-sx1262"
+ ],
+ "hwModels": [
+ 53
+ ],
+ "marketplace": null,
+ "regions": null
+ },
+ {
+ "shortCode": "seeed_xiao_nrf52840_kit",
+ "url": "https://msh.to/seeed_xiao_nrf52840_kit",
+ "description": "XIAO nRF52840 & Wio-SX1262 Kit",
+ "type": "vendor",
+ "targets": [
+ "seeed_xiao_nrf52840_kit"
+ ],
+ "hwModels": [
+ 88
+ ],
+ "marketplace": null,
+ "regions": null
+ },
+ {
+ "shortCode": "seeed_xiao_nrf52840_kit_aliexpress",
+ "url": "https://msh.to/seeed_xiao_nrf52840_kit_aliexpress",
+ "description": "XIAO nRF52840 & Wio-SX1262 Kit Aliexpress",
+ "type": "marketplace",
+ "targets": [
+ "seeed_xiao_nrf52840_kit"
+ ],
+ "hwModels": [
+ 88
+ ],
+ "marketplace": "aliexpress",
+ "regions": null
+ },
+ {
+ "shortCode": "thinknode_m1",
+ "url": "https://msh.to/thinknode_m1",
+ "description": "ThinkNode M1",
+ "type": "vendor",
+ "targets": [
+ "thinknode_m1"
+ ],
+ "hwModels": [
+ 89
+ ],
+ "marketplace": null,
+ "regions": null
+ },
+ {
+ "shortCode": "thinknode_m2",
+ "url": "https://msh.to/thinknode_m2",
+ "description": "ThinkNode M2",
+ "type": "vendor",
+ "targets": [
+ "thinknode_m2"
+ ],
+ "hwModels": [
+ 90
+ ],
+ "marketplace": null,
+ "regions": null
+ },
+ {
+ "shortCode": "thinknode_m3",
+ "url": "https://msh.to/thinknode_m3",
+ "description": "ThinkNode M3",
+ "type": "vendor",
+ "targets": [
+ "thinknode_m3"
+ ],
+ "hwModels": [
+ 115
+ ],
+ "marketplace": null,
+ "regions": null
+ },
+ {
+ "shortCode": "thinknode_m5",
+ "url": "https://msh.to/thinknode_m5",
+ "description": "ThinkNode M5",
+ "type": "vendor",
+ "targets": [
+ "thinknode_m5"
+ ],
+ "hwModels": [
+ 107
+ ],
+ "marketplace": null,
+ "regions": null
+ },
+ {
+ "shortCode": "thinknode_m4",
+ "url": "https://msh.to/thinknode_m4",
+ "description": "ThinkNode M4",
+ "type": "vendor",
+ "targets": [
+ "thinknode_m4"
+ ],
+ "hwModels": [
+ 119
+ ],
+ "marketplace": null,
+ "regions": null
+ },
+ {
+ "shortCode": "thinknode_m6",
+ "url": "https://msh.to/thinknode_m6",
+ "description": "ThinkNode M6",
+ "type": "vendor",
+ "targets": [
+ "thinknode_m6"
+ ],
+ "hwModels": [
+ 120
+ ],
+ "marketplace": null,
+ "regions": null
+ },
+ {
+ "shortCode": "heltec-mesh-pocket-10000",
+ "url": "https://msh.to/heltec-mesh-pocket-10000",
+ "description": "MeshPocket",
+ "type": "vendor",
+ "targets": [
+ "heltec-mesh-pocket-10000"
+ ],
+ "hwModels": [
+ 94
+ ],
+ "marketplace": null,
+ "regions": null
+ },
+ {
+ "shortCode": "seeed_solar_node",
+ "url": "https://msh.to/seeed_solar_node",
+ "description": "SenseCAP Solar Node P1 Pro",
+ "type": "vendor",
+ "targets": [
+ "seeed_solar_node"
+ ],
+ "hwModels": [
+ 95
+ ],
+ "marketplace": null,
+ "regions": null
+ },
+ {
+ "shortCode": "seeed_solar_node_aliexpress",
+ "url": "https://msh.to/seeed_solar_node_aliexpress",
+ "description": "SenseCAP Solar Node P1 Pro Aliexpress",
+ "type": "marketplace",
+ "targets": [
+ "seeed_solar_node"
+ ],
+ "hwModels": [
+ 95
+ ],
+ "marketplace": "aliexpress",
+ "regions": null
+ },
+ {
+ "shortCode": "seeed_solar_node_amazon",
+ "url": "https://msh.to/seeed_solar_node_amazon",
+ "description": "SenseCAP Solar Node P1 Pro Amazon",
+ "type": "marketplace",
+ "targets": [
+ "seeed_solar_node"
+ ],
+ "hwModels": [
+ 95
+ ],
+ "marketplace": "amazon",
+ "regions": [
+ "AU",
+ "CA",
+ "FR",
+ "DE",
+ "IE",
+ "JP",
+ "NL",
+ "ES",
+ "SE",
+ "GB",
+ "US"
+ ]
+ },
+ {
+ "shortCode": "elecrow-adv-35-tft",
+ "url": "https://msh.to/elecrow-adv-35-tft",
+ "description": "CrowPanel 3.5",
+ "type": "vendor",
+ "targets": [
+ "elecrow-adv-35-tft"
+ ],
+ "hwModels": [
+ 97
+ ],
+ "marketplace": null,
+ "regions": null
+ },
+ {
+ "shortCode": "elecrow-adv1-43-50-70-tft",
+ "url": "https://msh.to/elecrow-adv1-43-50-70-tft",
+ "description": "CrowPanel 4.3",
+ "type": "vendor",
+ "targets": [
+ "elecrow-adv1-43-50-70-tft"
+ ],
+ "hwModels": [
+ 97
+ ],
+ "marketplace": null,
+ "regions": null
+ },
+ {
+ "shortCode": "elecrow-adv-24-28-tft",
+ "url": "https://msh.to/elecrow-adv-24-28-tft",
+ "description": "CrowPanel 2.4",
+ "type": "vendor",
+ "targets": [
+ "elecrow-adv-24-28-tft"
+ ],
+ "hwModels": [
+ 97
+ ],
+ "marketplace": null,
+ "regions": null
+ },
+ {
+ "shortCode": "elecrow-adv-28-tft",
+ "url": "https://msh.to/elecrow-adv-28-tft",
+ "description": "CrowPanel 2.8",
+ "type": "vendor",
+ "targets": [
+ "elecrow-adv-24-28-tft"
+ ],
+ "hwModels": [
+ 97
+ ],
+ "marketplace": null,
+ "regions": null
+ },
+ {
+ "shortCode": "elecrow-adv1-50-tft",
+ "url": "https://msh.to/elecrow-adv1-50-tft",
+ "description": "CrowPanel 5.0",
+ "type": "vendor",
+ "targets": [
+ "elecrow-adv1-43-50-70-tft"
+ ],
+ "hwModels": [
+ 97
+ ],
+ "marketplace": null,
+ "regions": null
+ },
+ {
+ "shortCode": "elecrow-adv1-70-tft",
+ "url": "https://msh.to/elecrow-adv1-70-tft",
+ "description": "CrowPanel 7.0",
+ "type": "vendor",
+ "targets": [
+ "elecrow-adv1-43-50-70-tft"
+ ],
+ "hwModels": [
+ 97
+ ],
+ "marketplace": null,
+ "regions": null
+ },
+ {
+ "shortCode": "seeed_wio_tracker_L1",
+ "url": "https://msh.to/seeed_wio_tracker_L1",
+ "description": "Wio Tracker L1",
+ "type": "vendor",
+ "targets": [
+ "seeed_wio_tracker_L1"
+ ],
+ "hwModels": [
+ 99
+ ],
+ "marketplace": null,
+ "regions": null
+ },
+ {
+ "shortCode": "seeed_wio_tracker_L1_aliexpress",
+ "url": "https://msh.to/seeed_wio_tracker_L1_aliexpress",
+ "description": "Wio Tracker L1 Aliexpress",
+ "type": "marketplace",
+ "targets": [
+ "seeed_wio_tracker_L1"
+ ],
+ "hwModels": [
+ 99
+ ],
+ "marketplace": "aliexpress",
+ "regions": null
+ },
+ {
+ "shortCode": "seeed_wio_tracker_L1_amazon",
+ "url": "https://msh.to/seeed_wio_tracker_L1_amazon",
+ "description": "Wio Tracker L1 Amazon",
+ "type": "marketplace",
+ "targets": [
+ "seeed_wio_tracker_L1"
+ ],
+ "hwModels": [
+ 99
+ ],
+ "marketplace": "amazon",
+ "regions": [
+ "AU",
+ "CA",
+ "FR",
+ "DE",
+ "IE",
+ "JP",
+ "NL",
+ "ES",
+ "SE",
+ "GB",
+ "US"
+ ]
+ },
+ {
+ "shortCode": "nano-g2-ultra",
+ "url": "https://msh.to/nano-g2-ultra",
+ "description": "Nano G2 Ultra",
+ "type": "vendor",
+ "targets": [
+ "nano-g2-ultra"
+ ],
+ "hwModels": [
+ 18
+ ],
+ "marketplace": null,
+ "regions": null
+ },
+ {
+ "shortCode": "rak11310",
+ "url": "https://msh.to/rak11310",
+ "description": "RAK11310",
+ "type": "vendor",
+ "targets": [
+ "rak11310"
+ ],
+ "hwModels": [
+ 26
+ ],
+ "marketplace": null,
+ "regions": null
+ },
+ {
+ "shortCode": "rokland-rak11310",
+ "url": "https://msh.to/rokland-rak11310",
+ "description": "Rokland RAKwireless RAK11310 WisBlock RP2040 Core Module",
+ "type": "vendor",
+ "targets": [
+ "rak11310"
+ ],
+ "hwModels": [
+ 26
+ ],
+ "marketplace": null,
+ "regions": null
+ },
+ {
+ "shortCode": "station-g2-tindie",
+ "url": "https://msh.to/station-g2-tindie",
+ "description": "Station G2 Tindie Listing",
+ "type": "marketplace",
+ "targets": [
+ "station-g2"
+ ],
+ "hwModels": [
+ 31
+ ],
+ "marketplace": "tindie",
+ "regions": [
+ "US",
+ "CA",
+ "GB",
+ "DE",
+ "FR",
+ "AU",
+ "NL"
+ ]
+ },
+ {
+ "shortCode": "nano-g2-ultra-tindie",
+ "url": "https://msh.to/nano-g2-ultra-tindie",
+ "description": "Nano G2 Ultra Tindie Listing",
+ "type": "marketplace",
+ "targets": [
+ "nano-g2-ultra"
+ ],
+ "hwModels": [
+ 18
+ ],
+ "marketplace": "tindie",
+ "regions": [
+ "US",
+ "CA",
+ "GB",
+ "DE",
+ "FR",
+ "AU",
+ "NL"
+ ]
+ },
+ {
+ "shortCode": "t-deck-plus",
+ "url": "https://msh.to/t-deck-plus",
+ "description": "T-Deck Plus",
+ "type": "vendor",
+ "targets": [
+ "t-deck"
+ ],
+ "hwModels": [
+ 50
+ ],
+ "marketplace": null,
+ "regions": null
+ },
+ {
+ "shortCode": "rokland-meshtastic-starter-kit",
+ "url": "https://msh.to/rokland-meshtastic-starter-kit",
+ "description": "Rokland Meshtastic Starter Kit",
+ "type": "marketplace",
+ "targets": null,
+ "hwModels": null,
+ "marketplace": "rokland",
+ "regions": [
+ "AU",
+ "AT",
+ "BE",
+ "CA",
+ "DK",
+ "EC",
+ "FR",
+ "DE",
+ "IE",
+ "JP",
+ "NL",
+ "NZ",
+ "NO",
+ "PK",
+ "ES",
+ "SE",
+ "CH",
+ "GB",
+ "US"
+ ]
+ },
+ {
+ "shortCode": "rokland-t-deck-base",
+ "url": "https://msh.to/rokland-t-deck-base",
+ "description": "Rokland T-Deck Base",
+ "type": "marketplace",
+ "targets": [
+ "t-deck"
+ ],
+ "hwModels": [
+ 50
+ ],
+ "marketplace": "rokland",
+ "regions": [
+ "AU",
+ "AT",
+ "BE",
+ "CA",
+ "DK",
+ "EC",
+ "FR",
+ "DE",
+ "IE",
+ "JP",
+ "NL",
+ "NZ",
+ "NO",
+ "PK",
+ "ES",
+ "SE",
+ "CH",
+ "GB",
+ "US"
+ ]
+ },
+ {
+ "shortCode": "rokland-t-deck-complete",
+ "url": "https://msh.to/rokland-t-deck-complete",
+ "description": "Rokland T-Deck Complete",
+ "type": "marketplace",
+ "targets": [
+ "t-deck"
+ ],
+ "hwModels": [
+ 50
+ ],
+ "marketplace": "rokland",
+ "regions": [
+ "AU",
+ "AT",
+ "BE",
+ "CA",
+ "DK",
+ "EC",
+ "FR",
+ "DE",
+ "IE",
+ "JP",
+ "NL",
+ "NZ",
+ "NO",
+ "PK",
+ "ES",
+ "SE",
+ "CH",
+ "GB",
+ "US"
+ ]
+ },
+ {
+ "shortCode": "rokland-t-deck-plus",
+ "url": "https://msh.to/rokland-t-deck-plus",
+ "description": "Rokland T-Deck Plus",
+ "type": "marketplace",
+ "targets": [
+ "t-deck"
+ ],
+ "hwModels": [
+ 50
+ ],
+ "marketplace": "rokland",
+ "regions": [
+ "AU",
+ "AT",
+ "BE",
+ "CA",
+ "DK",
+ "EC",
+ "FR",
+ "DE",
+ "IE",
+ "JP",
+ "NL",
+ "NZ",
+ "NO",
+ "PK",
+ "ES",
+ "SE",
+ "CH",
+ "GB",
+ "US"
+ ]
+ },
+ {
+ "shortCode": "rokland-t-echo",
+ "url": "https://msh.to/rokland-t-echo",
+ "description": "Rokland T-Echo",
+ "type": "marketplace",
+ "targets": [
+ "t-echo"
+ ],
+ "hwModels": [
+ 7
+ ],
+ "marketplace": "rokland",
+ "regions": [
+ "AU",
+ "AT",
+ "BE",
+ "CA",
+ "DK",
+ "EC",
+ "FR",
+ "DE",
+ "IE",
+ "JP",
+ "NL",
+ "NZ",
+ "NO",
+ "PK",
+ "ES",
+ "SE",
+ "CH",
+ "GB",
+ "US"
+ ]
+ },
+ {
+ "shortCode": "rokland-t-echo-bme280",
+ "url": "https://msh.to/rokland-t-echo-bme280",
+ "description": "Rokland T-Echo with BME280",
+ "type": "marketplace",
+ "targets": [
+ "t-echo"
+ ],
+ "hwModels": [
+ 7
+ ],
+ "marketplace": "rokland",
+ "regions": [
+ "AU",
+ "AT",
+ "BE",
+ "CA",
+ "DK",
+ "EC",
+ "FR",
+ "DE",
+ "IE",
+ "JP",
+ "NL",
+ "NZ",
+ "NO",
+ "PK",
+ "ES",
+ "SE",
+ "CH",
+ "GB",
+ "US"
+ ]
+ },
+ {
+ "shortCode": "rokland-rak19007",
+ "url": "https://msh.to/rokland-rak19007",
+ "description": "Rokland RAKwireless RAK19007 WisBlock Base Board 2nd Gen",
+ "type": "marketplace",
+ "targets": [],
+ "hwModels": [],
+ "marketplace": "rokland",
+ "regions": [
+ "AU",
+ "AT",
+ "BE",
+ "CA",
+ "DK",
+ "EC",
+ "FR",
+ "DE",
+ "IE",
+ "JP",
+ "NL",
+ "NZ",
+ "NO",
+ "PK",
+ "ES",
+ "SE",
+ "CH",
+ "GB",
+ "US"
+ ]
+ },
+ {
+ "shortCode": "hexaspot-rak19007",
+ "url": "https://msh.to/hexaspot-rak19007",
+ "description": "Hexaspot RAKwireless RAK19007 WisBlock Base Board 2nd Gen",
+ "type": "marketplace",
+ "targets": [],
+ "hwModels": [],
+ "marketplace": "hexaspot",
+ "regions": [
+ "AT",
+ "BE",
+ "BG",
+ "CY",
+ "CZ",
+ "DE",
+ "DK",
+ "EE",
+ "ES",
+ "FI",
+ "FR",
+ "GR",
+ "HR",
+ "HU",
+ "IE",
+ "IT",
+ "LT",
+ "LU",
+ "LV",
+ "MT",
+ "NL",
+ "NO",
+ "PL",
+ "PT",
+ "RO",
+ "SE",
+ "SI",
+ "SK"
+ ]
+ },
+ {
+ "shortCode": "rokland-starter-kit",
+ "url": "https://msh.to/rokland-starter-kit",
+ "description": "Rokland RAKwireless 4631 Starter Kit",
+ "type": "marketplace",
+ "targets": [
+ "rak4631"
+ ],
+ "hwModels": [
+ 9
+ ],
+ "marketplace": "rokland",
+ "regions": [
+ "AU",
+ "AT",
+ "BE",
+ "CA",
+ "DK",
+ "EC",
+ "FR",
+ "DE",
+ "IE",
+ "JP",
+ "NL",
+ "NZ",
+ "NO",
+ "PK",
+ "ES",
+ "SE",
+ "CH",
+ "GB",
+ "US"
+ ]
+ },
+ {
+ "shortCode": "hexaspot-starter-kit",
+ "url": "https://msh.to/hexaspot-starter-kit",
+ "description": "Hexaspot RAKwireless 4631 Starter Kit",
+ "type": "marketplace",
+ "targets": [
+ "rak4631"
+ ],
+ "hwModels": [
+ 9
+ ],
+ "marketplace": "hexaspot",
+ "regions": [
+ "AT",
+ "BE",
+ "BG",
+ "CY",
+ "CZ",
+ "DE",
+ "DK",
+ "EE",
+ "ES",
+ "FI",
+ "FR",
+ "GR",
+ "HR",
+ "HU",
+ "IE",
+ "IT",
+ "LT",
+ "LU",
+ "LV",
+ "MT",
+ "NL",
+ "NO",
+ "PL",
+ "PT",
+ "RO",
+ "SE",
+ "SI",
+ "SK"
+ ]
+ },
+ {
+ "shortCode": "aliexpress-rak1921",
+ "url": "https://msh.to/aliexpress-rak1921",
+ "description": "RAK1921 OLED Display (AliExpress)",
+ "type": "marketplace",
+ "targets": [],
+ "hwModels": [],
+ "marketplace": "aliexpress",
+ "regions": null
+ },
+ {
+ "shortCode": "rak1921",
+ "url": "https://msh.to/rak1921",
+ "description": "RAK1921 OLED Display (RAK Store)",
+ "type": "vendor",
+ "targets": [],
+ "hwModels": [],
+ "marketplace": null,
+ "regions": null
+ },
+ {
+ "shortCode": "rokland-rak1921",
+ "url": "https://msh.to/rokland-rak1921",
+ "description": "Rokland RAK1921 WisBlock OLED Display",
+ "type": "marketplace",
+ "targets": [],
+ "hwModels": [],
+ "marketplace": "rokland",
+ "regions": [
+ "AU",
+ "AT",
+ "BE",
+ "CA",
+ "DK",
+ "EC",
+ "FR",
+ "DE",
+ "IE",
+ "JP",
+ "NL",
+ "NZ",
+ "NO",
+ "PK",
+ "ES",
+ "SE",
+ "CH",
+ "GB",
+ "US"
+ ]
+ },
+ {
+ "shortCode": "muzi-rak1921",
+ "url": "https://msh.to/muzi-rak1921",
+ "description": "Muzi Works RAK1921 OLED Display SSD1306",
+ "type": "marketplace",
+ "targets": [],
+ "hwModels": [],
+ "marketplace": "muzi",
+ "regions": [
+ "AU",
+ "AT",
+ "BE",
+ "CA",
+ "CZ",
+ "DK",
+ "FI",
+ "FR",
+ "DE",
+ "HK",
+ "IN",
+ "IE",
+ "IL",
+ "IT",
+ "JP",
+ "MY",
+ "NL",
+ "NZ",
+ "NO",
+ "PL",
+ "PT",
+ "SG",
+ "KR",
+ "ES",
+ "SE",
+ "CH",
+ "TW",
+ "AE",
+ "GB",
+ "US"
+ ]
+ },
+ {
+ "shortCode": "aliexpress-rak14000",
+ "url": "https://msh.to/aliexpress-rak14000",
+ "description": "RAK14000 E-Ink Display (AliExpress)",
+ "type": "marketplace",
+ "targets": [],
+ "hwModels": [],
+ "marketplace": "aliexpress",
+ "regions": null
+ },
+ {
+ "shortCode": "rak14000",
+ "url": "https://msh.to/rak14000",
+ "description": "RAK14000 E-Ink Display (RAK Store)",
+ "type": "vendor",
+ "targets": [],
+ "hwModels": [],
+ "marketplace": null,
+ "regions": null
+ },
+ {
+ "shortCode": "rokland-rak14000",
+ "url": "https://msh.to/rokland-rak14000",
+ "description": "Rokland RAK14000 WisBlock E-Ink Display",
+ "type": "marketplace",
+ "targets": [],
+ "hwModels": [],
+ "marketplace": "rokland",
+ "regions": [
+ "AU",
+ "AT",
+ "BE",
+ "CA",
+ "DK",
+ "EC",
+ "FR",
+ "DE",
+ "IE",
+ "JP",
+ "NL",
+ "NZ",
+ "NO",
+ "PK",
+ "ES",
+ "SE",
+ "CH",
+ "GB",
+ "US"
+ ]
+ },
+ {
+ "shortCode": "aliexpress-rak12500",
+ "url": "https://msh.to/aliexpress-rak12500",
+ "description": "RAK12500 (AliExpress)",
+ "type": "marketplace",
+ "targets": [],
+ "hwModels": [],
+ "marketplace": "aliexpress",
+ "regions": null
+ },
+ {
+ "shortCode": "rak12500",
+ "url": "https://msh.to/rak12500",
+ "description": "RAK12500 (RAK Store)",
+ "type": "vendor",
+ "targets": [],
+ "hwModels": [],
+ "marketplace": null,
+ "regions": null
+ },
+ {
+ "shortCode": "rak13300",
+ "url": "https://msh.to/rak13300",
+ "description": "RAK13300 LPWAN Module (RAK Store)",
+ "type": "vendor",
+ "targets": [],
+ "hwModels": [],
+ "marketplace": null,
+ "regions": null
+ },
+ {
+ "shortCode": "aliexpress-rak13002",
+ "url": "https://msh.to/aliexpress-rak13002",
+ "description": "RAK13002 IO Module (AliExpress)",
+ "type": "marketplace",
+ "targets": [],
+ "hwModels": [],
+ "marketplace": "aliexpress",
+ "regions": null
+ },
+ {
+ "shortCode": "rak13002",
+ "url": "https://msh.to/rak13002",
+ "description": "RAK13002 IO Module (RAK Store)",
+ "type": "vendor",
+ "targets": [],
+ "hwModels": [],
+ "marketplace": null,
+ "regions": null
+ },
+ {
+ "shortCode": "rokland-rak13002",
+ "url": "https://msh.to/rokland-rak13002",
+ "description": "Rokland RAK13002 WisBlock IO Adapter Module",
+ "type": "marketplace",
+ "targets": [],
+ "hwModels": [],
+ "marketplace": "rokland",
+ "regions": [
+ "AU",
+ "AT",
+ "BE",
+ "CA",
+ "DK",
+ "EC",
+ "FR",
+ "DE",
+ "IE",
+ "JP",
+ "NL",
+ "NZ",
+ "NO",
+ "PK",
+ "ES",
+ "SE",
+ "CH",
+ "GB",
+ "US"
+ ]
+ },
+ {
+ "shortCode": "muzi-rak13002",
+ "url": "https://msh.to/muzi-rak13002",
+ "description": "Muzi Works RAK13002 IO Module",
+ "type": "marketplace",
+ "targets": [],
+ "hwModels": [],
+ "marketplace": "muzi",
+ "regions": [
+ "AU",
+ "AT",
+ "BE",
+ "CA",
+ "CZ",
+ "DK",
+ "FI",
+ "FR",
+ "DE",
+ "HK",
+ "IN",
+ "IE",
+ "IL",
+ "IT",
+ "JP",
+ "MY",
+ "NL",
+ "NZ",
+ "NO",
+ "PL",
+ "PT",
+ "SG",
+ "KR",
+ "ES",
+ "SE",
+ "CH",
+ "TW",
+ "AE",
+ "GB",
+ "US"
+ ]
+ },
+ {
+ "shortCode": "rak6421",
+ "url": "https://msh.to/rak6421",
+ "description": "WisMesh Pi Hat RAK6421 (RAK Store)",
+ "type": "vendor",
+ "targets": [],
+ "hwModels": [],
+ "marketplace": null,
+ "regions": null
+ },
+ {
+ "shortCode": "aliexpress-rak18001",
+ "url": "https://msh.to/aliexpress-rak18001",
+ "description": "RAK18001 RAK Buzzer (AliExpress)",
+ "type": "marketplace",
+ "targets": [],
+ "hwModels": [],
+ "marketplace": "aliexpress",
+ "regions": null
+ },
+ {
+ "shortCode": "rak18001",
+ "url": "https://msh.to/rak18001",
+ "description": "RAK18001 RAK Buzzer (RAK Store)",
+ "type": "vendor",
+ "targets": [],
+ "hwModels": [],
+ "marketplace": null,
+ "regions": null
+ },
+ {
+ "shortCode": "aliexpress-rak1901",
+ "url": "https://msh.to/aliexpress-rak1901",
+ "description": "RAK1901 Temperature and Humidity Sensor (AliExpress)",
+ "type": "marketplace",
+ "targets": [],
+ "hwModels": [],
+ "marketplace": "aliexpress",
+ "regions": null
+ },
+ {
+ "shortCode": "rak1901",
+ "url": "https://msh.to/rak1901",
+ "description": "RAK1901 Temperature and Humidity Sensor (RAK Store)",
+ "type": "vendor",
+ "targets": [],
+ "hwModels": [],
+ "marketplace": null,
+ "regions": null
+ },
+ {
+ "shortCode": "aliexpress-rak1902",
+ "url": "https://msh.to/aliexpress-rak1902",
+ "description": "RAK-1902 Barometric Pressure Sensor (AliExpress)",
+ "type": "marketplace",
+ "targets": [],
+ "hwModels": [],
+ "marketplace": "aliexpress",
+ "regions": null
+ },
+ {
+ "shortCode": "rak1902",
+ "url": "https://msh.to/rak1902",
+ "description": "RAK-1902 Barometric Pressure Sensor (RAK Store)",
+ "type": "vendor",
+ "targets": [],
+ "hwModels": [],
+ "marketplace": null,
+ "regions": null
+ },
+ {
+ "shortCode": "aliexpress-rak1906",
+ "url": "https://msh.to/aliexpress-rak1906",
+ "description": "RAK1906 Environment Sensor (AliExpress)",
+ "type": "marketplace",
+ "targets": [],
+ "hwModels": [],
+ "marketplace": "aliexpress",
+ "regions": null
+ },
+ {
+ "shortCode": "rak1906",
+ "url": "https://msh.to/rak1906",
+ "description": "RAK1906 Environment Sensor (RAK Store)",
+ "type": "vendor",
+ "targets": [],
+ "hwModels": [],
+ "marketplace": null,
+ "regions": null
+ },
+ {
+ "shortCode": "aliexpress-rak12002",
+ "url": "https://msh.to/aliexpress-rak12002",
+ "description": "RAK12002 WisBlock RTC Module (AliExpress)",
+ "type": "marketplace",
+ "targets": [],
+ "hwModels": [],
+ "marketplace": "aliexpress",
+ "regions": null
+ },
+ {
+ "shortCode": "rak12002",
+ "url": "https://msh.to/rak12002",
+ "description": "RAK12002 WisBlock RTC Module (RAK Store)",
+ "type": "vendor",
+ "targets": [],
+ "hwModels": [],
+ "marketplace": null,
+ "regions": null
+ },
+ {
+ "shortCode": "rokland-rak12002",
+ "url": "https://msh.to/rokland-rak12002",
+ "description": "Rokland RAK12002 RTC Module Micro Crystal RV-3028-C7",
+ "type": "marketplace",
+ "targets": [],
+ "hwModels": [],
+ "marketplace": "rokland",
+ "regions": [
+ "AU",
+ "AT",
+ "BE",
+ "CA",
+ "DK",
+ "EC",
+ "FR",
+ "DE",
+ "IE",
+ "JP",
+ "NL",
+ "NZ",
+ "NO",
+ "PK",
+ "ES",
+ "SE",
+ "CH",
+ "GB",
+ "US"
+ ]
+ },
+ {
+ "shortCode": "aliexpress-wismesh-pocket-v2",
+ "url": "https://msh.to/aliexpress-wismesh-pocket-v2",
+ "description": "WisMesh Pocket V2 (AliExpress)",
+ "type": "marketplace",
+ "targets": null,
+ "hwModels": null,
+ "marketplace": "aliexpress",
+ "regions": null
+ },
+ {
+ "shortCode": "rokland-wismesh-pocket-v2",
+ "url": "https://msh.to/rokland-wismesh-pocket-v2",
+ "description": "WisMesh Pocket V2 (Rokland)",
+ "type": "marketplace",
+ "targets": null,
+ "hwModels": null,
+ "marketplace": "rokland",
+ "regions": [
+ "AU",
+ "AT",
+ "BE",
+ "CA",
+ "DK",
+ "EC",
+ "FR",
+ "DE",
+ "IE",
+ "JP",
+ "NL",
+ "NZ",
+ "NO",
+ "PK",
+ "ES",
+ "SE",
+ "CH",
+ "GB",
+ "US"
+ ]
+ },
+ {
+ "shortCode": "hexaspot-wismesh-pocket-v2",
+ "url": "https://msh.to/hexaspot-wismesh-pocket-v2",
+ "description": "WisMesh Pocket V2 (Hexaspot)",
+ "type": "marketplace",
+ "targets": null,
+ "hwModels": null,
+ "marketplace": "hexaspot",
+ "regions": [
+ "AT",
+ "BE",
+ "BG",
+ "CY",
+ "CZ",
+ "DE",
+ "DK",
+ "EE",
+ "ES",
+ "FI",
+ "FR",
+ "GR",
+ "HR",
+ "HU",
+ "IE",
+ "IT",
+ "LT",
+ "LU",
+ "LV",
+ "MT",
+ "NL",
+ "NO",
+ "PL",
+ "PT",
+ "RO",
+ "SE",
+ "SI",
+ "SK"
+ ]
+ },
+ {
+ "shortCode": "wismesh-pocket-v2",
+ "url": "https://msh.to/wismesh-pocket-v2",
+ "description": "WisMesh Pocket V2 (RAK Store)",
+ "type": "vendor",
+ "targets": null,
+ "hwModels": null,
+ "marketplace": null,
+ "regions": null
+ },
+ {
+ "shortCode": "aliexpress-wismesh-pocket-mini",
+ "url": "https://msh.to/aliexpress-wismesh-pocket-mini",
+ "description": "WisMesh Pocket Mini (Rokland)",
+ "type": "marketplace",
+ "targets": null,
+ "hwModels": null,
+ "marketplace": "aliexpress",
+ "regions": null
+ },
+ {
+ "shortCode": "rokland-wismesh-pocket-mini",
+ "url": "https://msh.to/rokland-wismesh-pocket-mini",
+ "description": "WisMesh Pocket Mini (Rokland)",
+ "type": "marketplace",
+ "targets": null,
+ "hwModels": null,
+ "marketplace": "rokland",
+ "regions": [
+ "AU",
+ "AT",
+ "BE",
+ "CA",
+ "DK",
+ "EC",
+ "FR",
+ "DE",
+ "IE",
+ "JP",
+ "NL",
+ "NZ",
+ "NO",
+ "PK",
+ "ES",
+ "SE",
+ "CH",
+ "GB",
+ "US"
+ ]
+ },
+ {
+ "shortCode": "wismesh-pocket-mini",
+ "url": "https://msh.to/wismesh-pocket-mini",
+ "description": "WisMesh Pocket Mini (RAK Store)",
+ "type": "vendor",
+ "targets": null,
+ "hwModels": null,
+ "marketplace": null,
+ "regions": null
+ },
+ {
+ "shortCode": "aliexpress-rak19026",
+ "url": "https://msh.to/aliexpress-rak19026",
+ "description": "WisMesh Baseboard (AliExpress)",
+ "type": "marketplace",
+ "targets": [],
+ "hwModels": [],
+ "marketplace": "aliexpress",
+ "regions": null
+ },
+ {
+ "shortCode": "rokland-rak19026",
+ "url": "https://msh.to/rokland-rak19026",
+ "description": "WisMesh Baseboard (Rokland)",
+ "type": "marketplace",
+ "targets": [],
+ "hwModels": [],
+ "marketplace": "rokland",
+ "regions": [
+ "AU",
+ "AT",
+ "BE",
+ "CA",
+ "DK",
+ "EC",
+ "FR",
+ "DE",
+ "IE",
+ "JP",
+ "NL",
+ "NZ",
+ "NO",
+ "PK",
+ "ES",
+ "SE",
+ "CH",
+ "GB",
+ "US"
+ ]
+ },
+ {
+ "shortCode": "rak19026",
+ "url": "https://msh.to/rak19026",
+ "description": "WisMesh Baseboard (RAK Store)",
+ "type": "vendor",
+ "targets": [],
+ "hwModels": [],
+ "marketplace": null,
+ "regions": null
+ },
+ {
+ "shortCode": "aliexpress-wismesh-tap",
+ "url": "https://msh.to/aliexpress-wismesh-tap",
+ "description": "RAK WisMesh Tap (AliExpress)",
+ "type": "marketplace",
+ "targets": [
+ "rak_wismeshtap"
+ ],
+ "hwModels": [
+ 84
+ ],
+ "marketplace": "aliexpress",
+ "regions": null
+ },
+ {
+ "shortCode": "aliexpress-board-one",
+ "url": "https://msh.to/aliexpress-board-one",
+ "description": "RAK WisMesh Board ONE (AliExpress)",
+ "type": "marketplace",
+ "targets": null,
+ "hwModels": null,
+ "marketplace": "aliexpress",
+ "regions": null
+ },
+ {
+ "shortCode": "board-one",
+ "url": "https://msh.to/board-one",
+ "description": "RAK WisMesh Board ONE (RAK Store)",
+ "type": "vendor",
+ "targets": null,
+ "hwModels": null,
+ "marketplace": null,
+ "regions": null
+ },
+ {
+ "shortCode": "rokland-board-one",
+ "url": "https://msh.to/rokland-board-one",
+ "description": "Rokland WisMesh Board ONE (US915 MHz)",
+ "type": "marketplace",
+ "targets": null,
+ "hwModels": null,
+ "marketplace": "rokland",
+ "regions": [
+ "AU",
+ "AT",
+ "BE",
+ "CA",
+ "DK",
+ "EC",
+ "FR",
+ "DE",
+ "IE",
+ "JP",
+ "NL",
+ "NZ",
+ "NO",
+ "PK",
+ "ES",
+ "SE",
+ "CH",
+ "GB",
+ "US"
+ ]
+ },
+ {
+ "shortCode": "wismesh-repeater",
+ "url": "https://msh.to/wismesh-repeater",
+ "description": "WisMesh Repeater (RAK Store)",
+ "type": "vendor",
+ "targets": [
+ "rak2560"
+ ],
+ "hwModels": [
+ 22
+ ],
+ "marketplace": null,
+ "regions": null
+ },
+ {
+ "shortCode": "aliexpress-wismesh-repeater",
+ "url": "https://msh.to/aliexpress-wismesh-repeater",
+ "description": "WisMesh Repeater (AliExpress)",
+ "type": "marketplace",
+ "targets": null,
+ "hwModels": null,
+ "marketplace": "aliexpress",
+ "regions": null
+ },
+ {
+ "shortCode": "aliexpress-wismesh-repeater-mini",
+ "url": "https://msh.to/aliexpress-wismesh-repeater-mini",
+ "description": "WisMesh Repeater Mini (AliExpress)",
+ "type": "marketplace",
+ "targets": null,
+ "hwModels": null,
+ "marketplace": "aliexpress",
+ "regions": null
+ },
+ {
+ "shortCode": "hexaspot-wismesh-repeater-mini",
+ "url": "https://msh.to/hexaspot-wismesh-repeater-mini",
+ "description": "WisMesh Repeater Mini (Hexaspot)",
+ "type": "marketplace",
+ "targets": null,
+ "hwModels": null,
+ "marketplace": "hexaspot",
+ "regions": [
+ "AT",
+ "BE",
+ "BG",
+ "CY",
+ "CZ",
+ "DE",
+ "DK",
+ "EE",
+ "ES",
+ "FI",
+ "FR",
+ "GR",
+ "HR",
+ "HU",
+ "IE",
+ "IT",
+ "LT",
+ "LU",
+ "LV",
+ "MT",
+ "NL",
+ "NO",
+ "PL",
+ "PT",
+ "RO",
+ "SE",
+ "SI",
+ "SK"
+ ]
+ },
+ {
+ "shortCode": "wismesh-repeater-mini",
+ "url": "https://msh.to/wismesh-repeater-mini",
+ "description": "WisMesh Repeater Mini (RAK Store)",
+ "type": "vendor",
+ "targets": null,
+ "hwModels": null,
+ "marketplace": null,
+ "regions": null
+ },
+ {
+ "shortCode": "aliexpress-wismesh-ethernet-gateway",
+ "url": "https://msh.to/aliexpress-wismesh-ethernet-gateway",
+ "description": "WisMesh Ethernet MQTT Gateway (AliExpress)",
+ "type": "marketplace",
+ "targets": null,
+ "hwModels": null,
+ "marketplace": "aliexpress",
+ "regions": null
+ },
+ {
+ "shortCode": "wismesh-ethernet-gateway",
+ "url": "https://msh.to/wismesh-ethernet-gateway",
+ "description": "WisMesh Ethernet MQTT Gateway (RAK Store)",
+ "type": "vendor",
+ "targets": null,
+ "hwModels": null,
+ "marketplace": null,
+ "regions": null
+ },
+ {
+ "shortCode": "aliexpress-wismesh-wifi-gateway",
+ "url": "https://msh.to/aliexpress-wismesh-wifi-gateway",
+ "description": "WisMesh WiFi MQTT Gateway (AliExpress)",
+ "type": "marketplace",
+ "targets": null,
+ "hwModels": null,
+ "marketplace": "aliexpress",
+ "regions": null
+ },
+ {
+ "shortCode": "wismesh-wifi-gateway",
+ "url": "https://msh.to/wismesh-wifi-gateway",
+ "description": "WisMesh WiFi MQTT Gateway (RAK Store)",
+ "type": "vendor",
+ "targets": null,
+ "hwModels": null,
+ "marketplace": null,
+ "regions": null
+ },
+ {
+ "shortCode": "aliexpress-board-one-pocket",
+ "url": "https://msh.to/aliexpress-board-one-pocket",
+ "description": "RAK WisMesh Board ONE Pocket (AliExpress)",
+ "type": "marketplace",
+ "targets": null,
+ "hwModels": null,
+ "marketplace": "aliexpress",
+ "regions": null
+ },
+ {
+ "shortCode": "board-one-pocket",
+ "url": "https://msh.to/board-one-pocket",
+ "description": "RAK WisMesh Board ONE Pocket (RAK Store)",
+ "type": "vendor",
+ "targets": null,
+ "hwModels": null,
+ "marketplace": null,
+ "regions": null
+ },
+ {
+ "shortCode": "aliexpress-wismesh-unify-enclosure",
+ "url": "https://msh.to/aliexpress-wismesh-unify-enclosure",
+ "description": "WisMesh Unify Enclosure (AliExpress)",
+ "type": "marketplace",
+ "targets": [],
+ "hwModels": [],
+ "marketplace": "aliexpress",
+ "regions": null
+ },
+ {
+ "shortCode": "wismesh-unify-enclosure",
+ "url": "https://msh.to/wismesh-unify-enclosure",
+ "description": "WisMesh Unify Enclosure (RAK Store)",
+ "type": "vendor",
+ "targets": [],
+ "hwModels": [],
+ "marketplace": null,
+ "regions": null
+ },
+ {
+ "shortCode": "aliexpress-wismesh-antenna",
+ "url": "https://msh.to/aliexpress-wismesh-antenna",
+ "description": "WisMesh Antenna (AliExpress)",
+ "type": "marketplace",
+ "targets": [],
+ "hwModels": [],
+ "marketplace": "aliexpress",
+ "regions": null
+ },
+ {
+ "shortCode": "wismesh-antenna",
+ "url": "https://msh.to/wismesh-antenna",
+ "description": "WisMesh Antenna (RAK Store)",
+ "type": "vendor",
+ "targets": [],
+ "hwModels": [],
+ "marketplace": null,
+ "regions": null
+ },
+ {
+ "shortCode": "muzi-rak4631",
+ "url": "https://msh.to/muzi-rak4631",
+ "description": "Muzi RAK4631 Starter Kit",
+ "type": "marketplace",
+ "targets": [
+ "rak4631"
+ ],
+ "hwModels": [
+ 9
+ ],
+ "marketplace": "muzi",
+ "regions": [
+ "AU",
+ "AT",
+ "BE",
+ "CA",
+ "CZ",
+ "DK",
+ "FI",
+ "FR",
+ "DE",
+ "HK",
+ "IN",
+ "IE",
+ "IL",
+ "IT",
+ "JP",
+ "MY",
+ "NL",
+ "NZ",
+ "NO",
+ "PL",
+ "PT",
+ "SG",
+ "KR",
+ "ES",
+ "SE",
+ "CH",
+ "TW",
+ "AE",
+ "GB",
+ "US"
+ ]
+ },
+ {
+ "shortCode": "aliexpress-rak19007",
+ "url": "https://msh.to/aliexpress-rak19007",
+ "description": "RAK19007 (AliExpress)",
+ "type": "marketplace",
+ "targets": [],
+ "hwModels": [],
+ "marketplace": "aliexpress",
+ "regions": null
+ },
+ {
+ "shortCode": "aliexpress-starter-kit",
+ "url": "https://msh.to/aliexpress-starter-kit",
+ "description": "WisMesh RAK4631 Starter Kit (AliExpress)",
+ "type": "marketplace",
+ "targets": [
+ "rak4631"
+ ],
+ "hwModels": [
+ 9
+ ],
+ "marketplace": "aliexpress",
+ "regions": null
+ },
+ {
+ "shortCode": "rak19003",
+ "url": "https://msh.to/rak19003",
+ "description": "RAK19003 (RAK Store)",
+ "type": "vendor",
+ "targets": [],
+ "hwModels": [],
+ "marketplace": null,
+ "regions": null
+ },
+ {
+ "shortCode": "aliexpress-rak19003",
+ "url": "https://msh.to/aliexpress-rak19003",
+ "description": "RAK19003 (AliExpress)",
+ "type": "marketplace",
+ "targets": [],
+ "hwModels": [],
+ "marketplace": "aliexpress",
+ "regions": null
+ },
+ {
+ "shortCode": "rak19001",
+ "url": "https://msh.to/rak19001",
+ "description": "RAK19001 WisBlock Dual IO Base Board (RAK Store)",
+ "type": "vendor",
+ "targets": [],
+ "hwModels": [],
+ "marketplace": null,
+ "regions": null
+ },
+ {
+ "shortCode": "aliexpress-rak19001",
+ "url": "https://msh.to/aliexpress-rak19001",
+ "description": "RAK19001 WisBlock Dual IO Base Board (AliExpress)",
+ "type": "marketplace",
+ "targets": [],
+ "hwModels": [],
+ "marketplace": "aliexpress",
+ "regions": null
+ },
+ {
+ "shortCode": "rokland-19003",
+ "url": "https://msh.to/rokland-19003",
+ "description": "Rokland WisBlock Mini Base Board RAK19003 (Ver B)",
+ "type": "marketplace",
+ "targets": [],
+ "hwModels": [],
+ "marketplace": "rokland",
+ "regions": [
+ "AU",
+ "AT",
+ "BE",
+ "CA",
+ "DK",
+ "EC",
+ "FR",
+ "DE",
+ "IE",
+ "JP",
+ "NL",
+ "NZ",
+ "NO",
+ "PK",
+ "ES",
+ "SE",
+ "CH",
+ "GB",
+ "US"
+ ]
+ },
+ {
+ "shortCode": "hexaspot-19003",
+ "url": "https://msh.to/hexaspot-19003",
+ "description": "Hexaspot WisBlock Mini Base Board RAK19003 (Ver B)",
+ "type": "marketplace",
+ "targets": [],
+ "hwModels": [],
+ "marketplace": "hexaspot",
+ "regions": [
+ "AT",
+ "BE",
+ "BG",
+ "CY",
+ "CZ",
+ "DE",
+ "DK",
+ "EE",
+ "ES",
+ "FI",
+ "FR",
+ "GR",
+ "HR",
+ "HU",
+ "IE",
+ "IT",
+ "LT",
+ "LU",
+ "LV",
+ "MT",
+ "NL",
+ "NO",
+ "PL",
+ "PT",
+ "RO",
+ "SE",
+ "SI",
+ "SK"
+ ]
+ },
+ {
+ "shortCode": "rokland-19001",
+ "url": "https://msh.to/rokland-19001",
+ "description": "Rokland WisBlock Dual IO Base Board RAK19001",
+ "type": "marketplace",
+ "targets": [],
+ "hwModels": [],
+ "marketplace": "rokland",
+ "regions": [
+ "AU",
+ "AT",
+ "BE",
+ "CA",
+ "DK",
+ "EC",
+ "FR",
+ "DE",
+ "IE",
+ "JP",
+ "NL",
+ "NZ",
+ "NO",
+ "PK",
+ "ES",
+ "SE",
+ "CH",
+ "GB",
+ "US"
+ ]
+ },
+ {
+ "shortCode": "hexaspot-19001",
+ "url": "https://msh.to/hexaspot-19001",
+ "description": "Hexaspot WisBlock Dual IO Base Board RAK19001",
+ "type": "marketplace",
+ "targets": [],
+ "hwModels": [],
+ "marketplace": "hexaspot",
+ "regions": [
+ "AT",
+ "BE",
+ "BG",
+ "CY",
+ "CZ",
+ "DE",
+ "DK",
+ "EE",
+ "ES",
+ "FI",
+ "FR",
+ "GR",
+ "HR",
+ "HU",
+ "IE",
+ "IT",
+ "LT",
+ "LU",
+ "LV",
+ "MT",
+ "NL",
+ "NO",
+ "PL",
+ "PT",
+ "RO",
+ "SE",
+ "SI",
+ "SK"
+ ]
+ },
+ {
+ "shortCode": "rokland-4631",
+ "url": "https://msh.to/rokland-4631",
+ "description": "Rokland RAK4631 Nordic nRF52840 BLE Core Module for LoRaWAN with LoRa SX1262",
+ "type": "marketplace",
+ "targets": [
+ "rak4631"
+ ],
+ "hwModels": [
+ 9
+ ],
+ "marketplace": "rokland",
+ "regions": [
+ "AU",
+ "AT",
+ "BE",
+ "CA",
+ "DK",
+ "EC",
+ "FR",
+ "DE",
+ "IE",
+ "JP",
+ "NL",
+ "NZ",
+ "NO",
+ "PK",
+ "ES",
+ "SE",
+ "CH",
+ "GB",
+ "US"
+ ]
+ },
+ {
+ "shortCode": "hexaspot-4631",
+ "url": "https://msh.to/hexaspot-4631",
+ "description": "Hexaspot RAK4631 Nordic nRF52840 BLE Core Module for LoRaWAN with LoRa SX1262",
+ "type": "marketplace",
+ "targets": [
+ "rak4631"
+ ],
+ "hwModels": [
+ 9
+ ],
+ "marketplace": "hexaspot",
+ "regions": [
+ "AT",
+ "BE",
+ "BG",
+ "CY",
+ "CZ",
+ "DE",
+ "DK",
+ "EE",
+ "ES",
+ "FI",
+ "FR",
+ "GR",
+ "HR",
+ "HU",
+ "IE",
+ "IT",
+ "LT",
+ "LU",
+ "LV",
+ "MT",
+ "NL",
+ "NO",
+ "PL",
+ "PT",
+ "RO",
+ "SE",
+ "SI",
+ "SK"
+ ]
+ },
+ {
+ "shortCode": "aliexpress-rak4631",
+ "url": "https://msh.to/aliexpress-rak4631",
+ "description": "RAK4631 Nordic nRF52840 BLE Core Module (AliExpress)",
+ "type": "marketplace",
+ "targets": [
+ "rak4631"
+ ],
+ "hwModels": [
+ 9
+ ],
+ "marketplace": "aliexpress",
+ "regions": null
+ },
+ {
+ "shortCode": "rakwireless-4631",
+ "url": "https://msh.to/rakwireless-4631",
+ "description": "RAK4631 Nordic nRF52840 BLE Core Module",
+ "type": "vendor",
+ "targets": [
+ "rak4631"
+ ],
+ "hwModels": [
+ 9
+ ],
+ "marketplace": null,
+ "regions": null
+ },
+ {
+ "shortCode": "rakwireless-rak11310",
+ "url": "https://msh.to/rakwireless-rak11310",
+ "description": "RAK11310 RP2040 Core Module)",
+ "type": "vendor",
+ "targets": [
+ "rak11310"
+ ],
+ "hwModels": [
+ 26
+ ],
+ "marketplace": null,
+ "regions": null
+ },
+ {
+ "shortCode": "rakwireless-rak3312",
+ "url": "https://msh.to/rakwireless-rak3312",
+ "description": "RAK3312 ESP32-S3 Core Module",
+ "type": "vendor",
+ "targets": [
+ "rak3312"
+ ],
+ "hwModels": [
+ 106
+ ],
+ "marketplace": null,
+ "regions": null
+ },
+ {
+ "shortCode": "hexaspot-rak3312",
+ "url": "https://msh.to/hexaspot-rak3312",
+ "description": "Hexaspot RAK3312 ESP32-S3 Core Module",
+ "type": "marketplace",
+ "targets": [
+ "rak3312"
+ ],
+ "hwModels": [
+ 106
+ ],
+ "marketplace": "hexaspot",
+ "regions": [
+ "AT",
+ "BE",
+ "BG",
+ "CY",
+ "CZ",
+ "DE",
+ "DK",
+ "EE",
+ "ES",
+ "FI",
+ "FR",
+ "GR",
+ "HR",
+ "HU",
+ "IE",
+ "IT",
+ "LT",
+ "LU",
+ "LV",
+ "MT",
+ "NL",
+ "NO",
+ "PL",
+ "PT",
+ "RO",
+ "SE",
+ "SI",
+ "SK"
+ ]
+ },
+ {
+ "shortCode": "rokland-rak3312",
+ "url": "https://msh.to/rokland-rak3312",
+ "description": "Rokland RAK3312 ESP32-S3 Core Module",
+ "type": "marketplace",
+ "targets": [
+ "rak3312"
+ ],
+ "hwModels": [
+ 106
+ ],
+ "marketplace": "rokland",
+ "regions": [
+ "AU",
+ "AT",
+ "BE",
+ "CA",
+ "DK",
+ "EC",
+ "FR",
+ "DE",
+ "IE",
+ "JP",
+ "NL",
+ "NZ",
+ "NO",
+ "PK",
+ "ES",
+ "SE",
+ "CH",
+ "GB",
+ "US"
+ ]
+ },
+ {
+ "shortCode": "rokland-rak3312-starter-kit",
+ "url": "https://msh.to/rokland-rak3312-starter-kit",
+ "description": "Rokland RAK3312 ESP32-S3 Starter Kit",
+ "type": "marketplace",
+ "targets": [
+ "rak3312"
+ ],
+ "hwModels": [
+ 106
+ ],
+ "marketplace": "rokland",
+ "regions": [
+ "AU",
+ "AT",
+ "BE",
+ "CA",
+ "DK",
+ "EC",
+ "FR",
+ "DE",
+ "IE",
+ "JP",
+ "NL",
+ "NZ",
+ "NO",
+ "PK",
+ "ES",
+ "SE",
+ "CH",
+ "GB",
+ "US"
+ ]
+ },
+ {
+ "shortCode": "aliexpress-rak11310",
+ "url": "https://msh.to/aliexpress-rak11310",
+ "description": "RAK11310 RP2040 Core Module (AliExpress)",
+ "type": "marketplace",
+ "targets": [
+ "rak11310"
+ ],
+ "hwModels": [
+ 26
+ ],
+ "marketplace": "aliexpress",
+ "regions": null
+ },
+ {
+ "shortCode": "rokland-1901",
+ "url": "https://msh.to/rokland-1901",
+ "description": "Rokland RAK1901 Temperature and Humidity Sensor",
+ "type": "marketplace",
+ "targets": [],
+ "hwModels": [],
+ "marketplace": "rokland",
+ "regions": [
+ "AU",
+ "AT",
+ "BE",
+ "CA",
+ "DK",
+ "EC",
+ "FR",
+ "DE",
+ "IE",
+ "JP",
+ "NL",
+ "NZ",
+ "NO",
+ "PK",
+ "ES",
+ "SE",
+ "CH",
+ "GB",
+ "US"
+ ]
+ },
+ {
+ "shortCode": "rokland-1902",
+ "url": "https://msh.to/rokland-1902",
+ "description": "Rokland RAK1902 Barometric Pressure Sensor",
+ "type": "marketplace",
+ "targets": [],
+ "hwModels": [],
+ "marketplace": "rokland",
+ "regions": [
+ "AU",
+ "AT",
+ "BE",
+ "CA",
+ "DK",
+ "EC",
+ "FR",
+ "DE",
+ "IE",
+ "JP",
+ "NL",
+ "NZ",
+ "NO",
+ "PK",
+ "ES",
+ "SE",
+ "CH",
+ "GB",
+ "US"
+ ]
+ },
+ {
+ "shortCode": "rokland-1906",
+ "url": "https://msh.to/rokland-1906",
+ "description": "Rokland RAK1906 WisBlock Environment Sensor",
+ "type": "marketplace",
+ "targets": [],
+ "hwModels": [],
+ "marketplace": "rokland",
+ "regions": [
+ "AU",
+ "AT",
+ "BE",
+ "CA",
+ "DK",
+ "EC",
+ "FR",
+ "DE",
+ "IE",
+ "JP",
+ "NL",
+ "NZ",
+ "NO",
+ "PK",
+ "ES",
+ "SE",
+ "CH",
+ "GB",
+ "US"
+ ]
+ },
+ {
+ "shortCode": "aliexpress-wismesh-tap",
+ "url": "https://msh.to/aliexpress-wismesh-tap",
+ "description": "RAKwireless WisMesh Tap (AliExpress)",
+ "type": "marketplace",
+ "targets": [
+ "rak_wismeshtap"
+ ],
+ "hwModels": [
+ 84
+ ],
+ "marketplace": "aliexpress",
+ "regions": null
+ },
+ {
+ "shortCode": "rokland-wismesh-tap",
+ "url": "https://msh.to/rokland-wismesh-tap",
+ "description": "RAKwireless WisMesh Tap (Rokland)",
+ "type": "marketplace",
+ "targets": [
+ "rak_wismeshtap"
+ ],
+ "hwModels": [
+ 84
+ ],
+ "marketplace": "rokland",
+ "regions": [
+ "AU",
+ "AT",
+ "BE",
+ "CA",
+ "DK",
+ "EC",
+ "FR",
+ "DE",
+ "IE",
+ "JP",
+ "NL",
+ "NZ",
+ "NO",
+ "PK",
+ "ES",
+ "SE",
+ "CH",
+ "GB",
+ "US"
+ ]
+ },
+ {
+ "shortCode": "rakdap1",
+ "url": "https://msh.to/rakdap1",
+ "description": "RAKwireless RAKDAP1 Debug and Flash Tool",
+ "type": "vendor",
+ "targets": [],
+ "hwModels": [],
+ "marketplace": null,
+ "regions": null
+ },
+ {
+ "shortCode": "rokland-heltec-wsl-v3",
+ "url": "https://msh.to/rokland-heltec-wsl-v3",
+ "description": "Rokland WSL V3",
+ "type": "marketplace",
+ "targets": [
+ "heltec-wsl-v3"
+ ],
+ "hwModels": [
+ 44
+ ],
+ "marketplace": "rokland",
+ "regions": [
+ "AU",
+ "AT",
+ "BE",
+ "CA",
+ "DK",
+ "EC",
+ "FR",
+ "DE",
+ "IE",
+ "JP",
+ "NL",
+ "NZ",
+ "NO",
+ "PK",
+ "ES",
+ "SE",
+ "CH",
+ "GB",
+ "US"
+ ]
+ },
+ {
+ "shortCode": "aliexpress-heltec-wsl-v3",
+ "url": "https://msh.to/aliexpress-heltec-wsl-v3",
+ "description": "Aliexpress WSL V3",
+ "type": "marketplace",
+ "targets": [
+ "heltec-wsl-v3"
+ ],
+ "hwModels": [
+ 44
+ ],
+ "marketplace": "aliexpress",
+ "regions": null
+ },
+ {
+ "shortCode": "rokland-heltec-wireless-tracker",
+ "url": "https://msh.to/rokland-heltec-wireless-tracker",
+ "description": "Rokland Wireless Tracker",
+ "type": "marketplace",
+ "targets": [
+ "heltec-wireless-tracker"
+ ],
+ "hwModels": [
+ 48
+ ],
+ "marketplace": "rokland",
+ "regions": [
+ "AU",
+ "AT",
+ "BE",
+ "CA",
+ "DK",
+ "EC",
+ "FR",
+ "DE",
+ "IE",
+ "JP",
+ "NL",
+ "NZ",
+ "NO",
+ "PK",
+ "ES",
+ "SE",
+ "CH",
+ "GB",
+ "US"
+ ]
+ },
+ {
+ "shortCode": "aliexpress-heltec-wireless-tracker",
+ "url": "https://msh.to/aliexpress-heltec-wireless-tracker",
+ "description": "Aliexpress Wireless Tracker",
+ "type": "marketplace",
+ "targets": [
+ "heltec-wireless-tracker"
+ ],
+ "hwModels": [
+ 48
+ ],
+ "marketplace": "aliexpress",
+ "regions": null
+ },
+ {
+ "shortCode": "aliexpress-heltec-wireless-paper",
+ "url": "https://msh.to/aliexpress-heltec-wireless-paper",
+ "description": "Aliexpress Wireless Paper",
+ "type": "marketplace",
+ "targets": [
+ "heltec-wireless-paper"
+ ],
+ "hwModels": [
+ 49
+ ],
+ "marketplace": "aliexpress",
+ "regions": null
+ },
+ {
+ "shortCode": "rokland-heltec-wireless-paper",
+ "url": "https://msh.to/rokland-heltec-wireless-paper",
+ "description": "Rokland Wireless Paper",
+ "type": "marketplace",
+ "targets": [
+ "heltec-wireless-paper"
+ ],
+ "hwModels": [
+ 49
+ ],
+ "marketplace": "rokland",
+ "regions": [
+ "AU",
+ "AT",
+ "BE",
+ "CA",
+ "DK",
+ "EC",
+ "FR",
+ "DE",
+ "IE",
+ "JP",
+ "NL",
+ "NZ",
+ "NO",
+ "PK",
+ "ES",
+ "SE",
+ "CH",
+ "GB",
+ "US"
+ ]
+ },
+ {
+ "shortCode": "muzi-heltec-mesh-node-t114",
+ "url": "https://msh.to/muzi-heltec-mesh-node-t114",
+ "description": "MuziWorks Mesh Node T114",
+ "type": "marketplace",
+ "targets": [
+ "heltec-mesh-node-t114"
+ ],
+ "hwModels": [
+ 69
+ ],
+ "marketplace": "muzi",
+ "regions": [
+ "AU",
+ "AT",
+ "BE",
+ "CA",
+ "CZ",
+ "DK",
+ "FI",
+ "FR",
+ "DE",
+ "HK",
+ "IN",
+ "IE",
+ "IL",
+ "IT",
+ "JP",
+ "MY",
+ "NL",
+ "NZ",
+ "NO",
+ "PL",
+ "PT",
+ "SG",
+ "KR",
+ "ES",
+ "SE",
+ "CH",
+ "TW",
+ "AE",
+ "GB",
+ "US"
+ ]
+ },
+ {
+ "shortCode": "aliexpress-heltec-mesh-node-t114",
+ "url": "https://msh.to/aliexpress-heltec-mesh-node-t114",
+ "description": "Aliexpress Mesh Node T114",
+ "type": "marketplace",
+ "targets": [
+ "heltec-mesh-node-t114"
+ ],
+ "hwModels": [
+ 69
+ ],
+ "marketplace": "aliexpress",
+ "regions": null
+ },
+ {
+ "shortCode": "aliexpress-heltec-vision-master-e213",
+ "url": "https://msh.to/aliexpress-heltec-vision-master-e213",
+ "description": "Aliexpress Vision Master E213",
+ "type": "marketplace",
+ "targets": [
+ "heltec-vision-master-e213"
+ ],
+ "hwModels": [
+ 67
+ ],
+ "marketplace": "aliexpress",
+ "regions": null
+ },
+ {
+ "shortCode": "aliexpress-heltec-vision-master-e290",
+ "url": "https://msh.to/aliexpress-heltec-vision-master-e290",
+ "description": "Aliexpress Vision Master E290",
+ "type": "marketplace",
+ "targets": [
+ "heltec-vision-master-e290"
+ ],
+ "hwModels": [
+ 68
+ ],
+ "marketplace": "aliexpress",
+ "regions": null
+ },
+ {
+ "shortCode": "aliexpress-heltec-vision-master-t190",
+ "url": "https://msh.to/aliexpress-heltec-vision-master-t190",
+ "description": "Aliexpress Vision Master T190",
+ "type": "marketplace",
+ "targets": [
+ "heltec-vision-master-t190"
+ ],
+ "hwModels": [
+ 66
+ ],
+ "marketplace": "aliexpress",
+ "regions": null
+ },
+ {
+ "shortCode": "seeed-wio-tracker-l1-oled",
+ "url": "https://msh.to/seeed-wio-tracker-l1-oled",
+ "description": "Wio Tracker L1 (with OLED)",
+ "type": "vendor",
+ "targets": [
+ "seeed_wio_tracker_L1"
+ ],
+ "hwModels": [
+ 99
+ ],
+ "marketplace": null,
+ "regions": null
+ },
+ {
+ "shortCode": "seeed-wio-tracker-l1-oled_aliexpress",
+ "url": "https://msh.to/seeed-wio-tracker-l1-oled_aliexpress",
+ "description": "Wio Tracker L1 (with OLED)",
+ "type": "marketplace",
+ "targets": [
+ "seeed_wio_tracker_L1"
+ ],
+ "hwModels": [
+ 99
+ ],
+ "marketplace": "aliexpress",
+ "regions": null
+ },
+ {
+ "shortCode": "seeed_wio_tracker_L1_eink",
+ "url": "https://msh.to/seeed_wio_tracker_L1_eink",
+ "description": "Wio Tracker L1 (with E-Ink)",
+ "type": "vendor",
+ "targets": [
+ "seeed_wio_tracker_L1_eink"
+ ],
+ "hwModels": [
+ 100
+ ],
+ "marketplace": null,
+ "regions": null
+ },
+ {
+ "shortCode": "seeed_wio_tracker_L1_eink_amazon",
+ "url": "https://msh.to/seeed_wio_tracker_L1_eink_amazon",
+ "description": "Wio Tracker L1 (with E-Ink) Amazon",
+ "type": "marketplace",
+ "targets": [
+ "seeed_wio_tracker_L1_eink"
+ ],
+ "hwModels": [
+ 100
+ ],
+ "marketplace": "amazon",
+ "regions": [
+ "AU",
+ "CA",
+ "FR",
+ "DE",
+ "IE",
+ "JP",
+ "NL",
+ "ES",
+ "SE",
+ "GB",
+ "US"
+ ]
+ },
+ {
+ "shortCode": "seeed-wio-tracker-l1-lite",
+ "url": "https://msh.to/seeed-wio-tracker-l1-lite",
+ "description": "Wio Tracker L1 Lite (no display)",
+ "type": "vendor",
+ "targets": null,
+ "hwModels": null,
+ "marketplace": null,
+ "regions": null
+ },
+ {
+ "shortCode": "seeed_solar_node_p1",
+ "url": "https://msh.to/seeed_solar_node_p1",
+ "description": "SenseCAP Solar Node P1",
+ "type": "vendor",
+ "targets": null,
+ "hwModels": null,
+ "marketplace": null,
+ "regions": null
+ },
+ {
+ "shortCode": "seeed_solar_node_p1_aliexpress",
+ "url": "https://msh.to/seeed_solar_node_p1_aliexpress",
+ "description": "SenseCAP Solar Node P1 Aliexpress",
+ "type": "marketplace",
+ "targets": null,
+ "hwModels": null,
+ "marketplace": "aliexpress",
+ "regions": null
+ },
+ {
+ "shortCode": "android-closed-test",
+ "url": "https://msh.to/android-closed-test",
+ "description": "Android Closed Test Form",
+ "type": "internal",
+ "targets": null,
+ "hwModels": null,
+ "marketplace": null,
+ "regions": null
+ },
+ {
+ "shortCode": "t-deck-pro",
+ "url": "https://msh.to/t-deck-pro",
+ "description": "LilyGo T-Deck Pro",
+ "type": "vendor",
+ "targets": [
+ "t-deck-pro"
+ ],
+ "hwModels": [
+ 102
+ ],
+ "marketplace": null,
+ "regions": null
+ },
+ {
+ "shortCode": "rak4631_nomadstar_meteor_pro",
+ "url": "https://msh.to/rak4631_nomadstar_meteor_pro",
+ "description": "NomadStar Meteor Pro",
+ "type": "vendor",
+ "targets": [
+ "rak4631_nomadstar_meteor_pro"
+ ],
+ "hwModels": [
+ 96
+ ],
+ "marketplace": null,
+ "regions": null
+ },
+ {
+ "shortCode": "muziworks",
+ "url": "https://msh.to/muziworks",
+ "description": "muzi WORKS Homepage",
+ "type": "internal",
+ "targets": null,
+ "hwModels": null,
+ "marketplace": null,
+ "regions": null
+ },
+ {
+ "shortCode": "r1-neo",
+ "url": "https://msh.to/r1-neo",
+ "description": "muzi WORKS R1 Neo",
+ "type": "vendor",
+ "targets": [
+ "r1-neo"
+ ],
+ "hwModels": [
+ 101
+ ],
+ "marketplace": null,
+ "regions": null
+ },
+ {
+ "shortCode": "muzi-base",
+ "url": "https://msh.to/muzi-base",
+ "description": "muzi WORKS Base System",
+ "type": "vendor",
+ "targets": [
+ "muzi-base"
+ ],
+ "hwModels": [
+ 93
+ ],
+ "marketplace": null,
+ "regions": null
+ },
+ {
+ "shortCode": "muzi-base-uno",
+ "url": "https://msh.to/muzi-base-uno",
+ "description": "muzi WORKS Base Uno",
+ "type": "vendor",
+ "targets": [
+ "muzi-base"
+ ],
+ "hwModels": [
+ 93
+ ],
+ "marketplace": null,
+ "regions": null
+ },
+ {
+ "shortCode": "muzi-base-duo",
+ "url": "https://msh.to/muzi-base-duo",
+ "description": "muzi WORKS Base Duo",
+ "type": "vendor",
+ "targets": [
+ "muzi-base"
+ ],
+ "hwModels": [
+ 93
+ ],
+ "marketplace": null,
+ "regions": null
+ },
+ {
+ "shortCode": "muzi-base-super-io",
+ "url": "https://msh.to/muzi-base-super-io",
+ "description": "muzi WORKS Base Super IO",
+ "type": "vendor",
+ "targets": [
+ "muzi-base"
+ ],
+ "hwModels": [
+ 93
+ ],
+ "marketplace": null,
+ "regions": null
+ },
+ {
+ "shortCode": "ttc-tickets",
+ "url": "https://msh.to/ttc-tickets",
+ "description": "The Things Conference Tickets",
+ "type": "internal",
+ "targets": null,
+ "hwModels": null,
+ "marketplace": null,
+ "regions": null
+ },
+ {
+ "shortCode": "rokland-atlavox-makers-market",
+ "url": "https://msh.to/rokland-atlavox-makers-market",
+ "description": "Rokland Atlavox Makers Market",
+ "type": "marketplace",
+ "targets": null,
+ "hwModels": null,
+ "marketplace": "rokland",
+ "regions": [
+ "AU",
+ "AT",
+ "BE",
+ "CA",
+ "DK",
+ "EC",
+ "FR",
+ "DE",
+ "IE",
+ "JP",
+ "NL",
+ "NZ",
+ "NO",
+ "PK",
+ "ES",
+ "SE",
+ "CH",
+ "GB",
+ "US"
+ ]
+ },
+ {
+ "shortCode": "rokland-tlora-pager",
+ "url": "https://msh.to/rokland-tlora-pager",
+ "description": "Rokland T-Lora Pager",
+ "type": "marketplace",
+ "targets": [
+ "tlora-pager"
+ ],
+ "hwModels": [
+ 103
+ ],
+ "marketplace": "rokland",
+ "regions": [
+ "AU",
+ "AT",
+ "BE",
+ "CA",
+ "DK",
+ "EC",
+ "FR",
+ "DE",
+ "IE",
+ "JP",
+ "NL",
+ "NZ",
+ "NO",
+ "PK",
+ "ES",
+ "SE",
+ "CH",
+ "GB",
+ "US"
+ ]
+ },
+ {
+ "shortCode": "tlora-pager",
+ "url": "https://msh.to/tlora-pager",
+ "description": "T-Lora Pager",
+ "type": "vendor",
+ "targets": [
+ "tlora-pager"
+ ],
+ "hwModels": [
+ 103
+ ],
+ "marketplace": null,
+ "regions": null
+ },
+ {
+ "shortCode": "hexaspot",
+ "url": "https://msh.to/hexaspot",
+ "description": "Hexaspot Meshtastic Products",
+ "type": "marketplace",
+ "targets": [],
+ "hwModels": [],
+ "marketplace": "hexaspot",
+ "regions": [
+ "AT",
+ "BE",
+ "BG",
+ "CY",
+ "CZ",
+ "DE",
+ "DK",
+ "EE",
+ "ES",
+ "FI",
+ "FR",
+ "GR",
+ "HR",
+ "HU",
+ "IE",
+ "IT",
+ "LT",
+ "LU",
+ "LV",
+ "MT",
+ "NL",
+ "NO",
+ "PL",
+ "PT",
+ "RO",
+ "SE",
+ "SI",
+ "SK"
+ ]
+ },
+ {
+ "shortCode": "ew26",
+ "url": "https://msh.to/ew26",
+ "description": "embeddedworld26 event page",
+ "type": "internal",
+ "targets": null,
+ "hwModels": null,
+ "marketplace": null,
+ "regions": null
+ },
+ {
+ "shortCode": "hexaspot-heltec-v3",
+ "url": "https://msh.to/hexaspot-heltec-v3",
+ "description": "Heltec V3 (Hexaspot)",
+ "type": "marketplace",
+ "targets": [
+ "heltec-v3"
+ ],
+ "hwModels": [
+ 43
+ ],
+ "marketplace": "hexaspot",
+ "regions": [
+ "AT",
+ "BE",
+ "BG",
+ "CY",
+ "CZ",
+ "DE",
+ "DK",
+ "EE",
+ "ES",
+ "FI",
+ "FR",
+ "GR",
+ "HR",
+ "HU",
+ "IE",
+ "IT",
+ "LT",
+ "LU",
+ "LV",
+ "MT",
+ "NL",
+ "NO",
+ "PL",
+ "PT",
+ "RO",
+ "SE",
+ "SI",
+ "SK"
+ ]
+ },
+ {
+ "shortCode": "hexaspot-heltec-v4",
+ "url": "https://msh.to/hexaspot-heltec-v4",
+ "description": "Heltec V4 (Hexaspot)",
+ "type": "marketplace",
+ "targets": [
+ "heltec-v4"
+ ],
+ "hwModels": [
+ 110
+ ],
+ "marketplace": "hexaspot",
+ "regions": [
+ "AT",
+ "BE",
+ "BG",
+ "CY",
+ "CZ",
+ "DE",
+ "DK",
+ "EE",
+ "ES",
+ "FI",
+ "FR",
+ "GR",
+ "HR",
+ "HU",
+ "IE",
+ "IT",
+ "LT",
+ "LU",
+ "LV",
+ "MT",
+ "NL",
+ "NO",
+ "PL",
+ "PT",
+ "RO",
+ "SE",
+ "SI",
+ "SK"
+ ]
+ },
+ {
+ "shortCode": "hexaspot-wireless-tracker-v2",
+ "url": "https://msh.to/hexaspot-wireless-tracker-v2",
+ "description": "Heltec Wireless Tracker V2 (Hexaspot)",
+ "type": "marketplace",
+ "targets": [
+ "heltec-wireless-tracker-v2"
+ ],
+ "hwModels": [
+ 113
+ ],
+ "marketplace": "hexaspot",
+ "regions": [
+ "AT",
+ "BE",
+ "BG",
+ "CY",
+ "CZ",
+ "DE",
+ "DK",
+ "EE",
+ "ES",
+ "FI",
+ "FR",
+ "GR",
+ "HR",
+ "HU",
+ "IE",
+ "IT",
+ "LT",
+ "LU",
+ "LV",
+ "MT",
+ "NL",
+ "NO",
+ "PL",
+ "PT",
+ "RO",
+ "SE",
+ "SI",
+ "SK"
+ ]
+ }
+ ]
+}
diff --git a/androidApp/src/main/assets/marketplaces.json b/androidApp/src/main/assets/marketplaces.json
deleted file mode 100644
index 49feb5c992..0000000000
--- a/androidApp/src/main/assets/marketplaces.json
+++ /dev/null
@@ -1,126 +0,0 @@
-{
- "rokland": {
- "regions": [
- "AU",
- "AT",
- "BE",
- "CA",
- "DK",
- "EC",
- "FR",
- "DE",
- "IE",
- "JP",
- "NL",
- "NZ",
- "NO",
- "PK",
- "ES",
- "SE",
- "CH",
- "GB",
- "US"
- ],
- "match": "prefix"
- },
- "hexaspot": {
- "regions": [
- "AT",
- "BE",
- "BG",
- "CY",
- "CZ",
- "DE",
- "DK",
- "EE",
- "ES",
- "FI",
- "FR",
- "GR",
- "HR",
- "HU",
- "IE",
- "IT",
- "LT",
- "LU",
- "LV",
- "MT",
- "NL",
- "NO",
- "PL",
- "PT",
- "RO",
- "SE",
- "SI",
- "SK"
- ],
- "match": "prefix"
- },
- "aliexpress": {
- "regions": [],
- "match": "suffix"
- },
- "amazon": {
- "regions": [
- "AU",
- "CA",
- "FR",
- "DE",
- "IE",
- "JP",
- "NL",
- "ES",
- "SE",
- "GB",
- "US"
- ],
- "match": "suffix"
- },
- "tindie": {
- "regions": [
- "US",
- "CA",
- "GB",
- "DE",
- "FR",
- "AU",
- "NL"
- ],
- "match": "suffix"
- },
- "muzi": {
- "regions": [
- "AU",
- "AT",
- "BE",
- "CA",
- "CZ",
- "DK",
- "FI",
- "FR",
- "DE",
- "HK",
- "IN",
- "IE",
- "IL",
- "IT",
- "JP",
- "MY",
- "NL",
- "NZ",
- "NO",
- "PL",
- "PT",
- "SG",
- "KR",
- "ES",
- "SE",
- "CH",
- "TW",
- "AE",
- "GB",
- "US"
- ],
- "match": "prefix"
- }
-}
\ No newline at end of file
diff --git a/androidApp/src/main/assets/urls.json b/androidApp/src/main/assets/urls.json
deleted file mode 100644
index 2b02b3fe63..0000000000
--- a/androidApp/src/main/assets/urls.json
+++ /dev/null
@@ -1,1009 +0,0 @@
-{
- "Routes": [
- {
- "ShortCode": "github",
- "OriginalUrl": "https://github.com/meshtastic",
- "Description": "Meshtastic GitHub Organization"
- },
- {
- "ShortCode": "youtube",
- "OriginalUrl": "https://www.youtube.com/meshtastic",
- "Description": "Meshtastic YouTube Channel"
- },
- {
- "ShortCode": "reddit",
- "OriginalUrl": "https://www.reddit.com/r/meshtastic",
- "Description": "Meshtastic Reddit Community"
- },
- {
- "ShortCode": "docs",
- "OriginalUrl": "https://meshtastic.org/docs/",
- "Description": "Meshtastic Documentation"
- },
- {
- "ShortCode": "discord",
- "OriginalUrl": "https://discord.gg/meshtastic",
- "Description": "Meshtastic Discord Server"
- },
- {
- "ShortCode": "web",
- "OriginalUrl": "https://client.meshtastic.org/",
- "Description": "Meshtastic Web Client"
- },
- {
- "ShortCode": "flash",
- "OriginalUrl": "https://flasher.meshtastic.org/",
- "Description": "Meshtastic Web Flasher"
- },
- {
- "ShortCode": "firmware",
- "OriginalUrl": "https://github.com/meshtastic/firmware",
- "Description": "Meshtastic Firmware Repository"
- },
- {
- "ShortCode": "android",
- "OriginalUrl": "https://play.google.com/store/apps/details?id=com.geeksville.mesh",
- "Description": "Meshtastic Android App"
- },
- {
- "ShortCode": "ios",
- "OriginalUrl": "https://apple.co/3Auysep",
- "Description": "Meshtastic iOS App"
- },
- {
- "ShortCode": "rak-collection",
- "OriginalUrl": "https://store.rakwireless.com/collections/meshtastic?utm_source=website_partner&utm_medium=referral&utm_campaign=meshtastic_rak_collab",
- "Description": "RAKwireless Meshtastic Collection"
- },
- {
- "ShortCode": "rak4631",
- "OriginalUrl": "https://store.rakwireless.com/products/wisblock-meshtastic-starter-kit?utm_source=website_partner&utm_medium=referral&utm_campaign=meshtastic_rak_collab",
- "Description": "WisMesh RAK4631 Starter Kit"
- },
- {
- "ShortCode": "rak3312",
- "OriginalUrl": "https://store.rakwireless.com/products/meshtastic-starter-kit-esp32-s3-lora-sx1262?utm_source=website_partner&utm_medium=referral&utm_campaign=meshtastic_rak_collab",
- "Description": "WisMesh ESP32-S3 Starter Kit"
- },
- {
- "ShortCode": "rak3401-1watt",
- "OriginalUrl": "https://store.rakwireless.com/products/wismesh-1w-booster-starter-kit?utm_source=website_partner&utm_medium=referral&utm_campaign=meshtastic_rak_collab",
- "Description": "WisMesh RAK3401 1W Starter Kit"
- },
- {
- "ShortCode": "rak_wismeshtap",
- "OriginalUrl": "https://store.rakwireless.com/products/wismesh-tap?utm_source=website_partner&utm_medium=referral&utm_campaign=meshtastic_rak_collab",
- "Description": "RAK WisMesh Tap"
- },
- {
- "ShortCode": "rak_wismeshtag",
- "OriginalUrl": "https://store.rakwireless.com/products/wismesh-tag-meshtastic-gps-lora-tracker-ip66?utm_source=website_partner&utm_medium=referral&utm_campaign=meshtastic_rak_collab",
- "Description": "RAK WisMesh Tag"
- },
- {
- "ShortCode": "rokland-wismesh-tag",
- "OriginalUrl": "https://store.rokland.com/products/wismesh-tag-from-rakwireless-mokosmart-meshtastic-compatible-card-sized-node-us915-mhz",
- "Description": "Rokland WisMesh Tag"
- },
- {
- "ShortCode": "hexaspot-wismesh-tag",
- "OriginalUrl": "https://hexaspot.com/collections/meshtastic-products/products/wismesh-tag",
- "Description": "Hexaspot WisMesh Tag"
- },
- {
- "ShortCode": "aliexpress-wismesh-tag",
- "OriginalUrl": "https://www.aliexpress.com/item/1005009754254701.html?utm_source=website_partner&utm_medium=referral&utm_campaign=meshtastic_rak_collab",
- "Description": "Aliexpress RAK WisMesh Tag"
- },
- {
- "ShortCode": "rak19007",
- "OriginalUrl": "https://store.rakwireless.com/products/rak19007-wisblock-base-board-2nd-gen?utm_source=website_partner&utm_medium=referral&utm_campaign=meshtastic_rak_collab",
- "Description": "RAKwireless RAK19007 WisBlock Base Board 2nd Gen"
- },
- {
- "ShortCode": "tbeam-s3-core",
- "OriginalUrl": "https://lilygo.cc/products/t-beam-supreme-meshtastic",
- "Description": "T-Beam Supreme"
- },
- {
- "ShortCode": "t-echo",
- "OriginalUrl": "https://lilygo.cc/products/t-echo-meshtastic",
- "Description": "T-Echo"
- },
- {
- "ShortCode": "t-watch-s3",
- "OriginalUrl": "https://lilygo.cc/products/t-watch-s3",
- "Description": "T-Watch S3"
- },
- {
- "ShortCode": "t-deck",
- "OriginalUrl": "https://lilygo.cc/products/t-deck-meshtastic",
- "Description": "T-Deck"
- },
- {
- "ShortCode": "tlora-t3s3-v1",
- "OriginalUrl": "https://lilygo.cc/products/t3-s3-meshtastic",
- "Description": "T3S3"
- },
- {
- "ShortCode": "heltec-mesh-node-t114",
- "OriginalUrl": "https://heltec.org/project/mesh-node-t114/",
- "Description": "Mesh Node T114"
- },
- {
- "ShortCode": "heltec-vision-master-e213",
- "OriginalUrl": "https://heltec.org/project/vision-master-e213/",
- "Description": "Vision Master E213"
- },
- {
- "ShortCode": "heltec-vision-master-e290",
- "OriginalUrl": "https://heltec.org/project/vision-master-e290/",
- "Description": "Vision Master E290"
- },
- {
- "ShortCode": "heltec-vision-master-t190",
- "OriginalUrl": "https://heltec.org/project/vision-master-t190/",
- "Description": "Vision Master T190"
- },
- {
- "ShortCode": "heltec-wireless-tracker",
- "OriginalUrl": "https://heltec.org/project/wireless-tracker/",
- "Description": "Wireless Tracker"
- },
- {
- "ShortCode": "heltec-wireless-tracker-v2",
- "OriginalUrl": "https://heltec.org/project/wireless-tracker-v2/",
- "Description": "Wireless Tracker V2"
- },
- {
- "ShortCode": "heltec-wireless-paper",
- "OriginalUrl": "https://heltec.org/project/wireless-paper/",
- "Description": "Wireless Paper"
- },
- {
- "ShortCode": "heltec-ht62-esp32c3-sx1262",
- "OriginalUrl": "https://heltec.org/project/ht-ct62/",
- "Description": "HT-CT62"
- },
- {
- "ShortCode": "wio-tracker-wm1110",
- "OriginalUrl": "https://www.seeedstudio.com/Wio-Tracker-1110-Dev-Kit-for-Meshtastic.html",
- "Description": "Wio Tracker WM1110 Dev Kit"
- },
- {
- "ShortCode": "tracker-t1000-e",
- "OriginalUrl": "https://www.seeedstudio.com/SenseCAP-Card-Tracker-T1000-E-for-Meshtastic-p-5913.html",
- "Description": "SenseCAP Card Tracker T1000-E"
- },
- {
- "ShortCode": "tracker-t1000-e-aliexpress",
- "OriginalUrl": "https://www.aliexpress.us/item/3256807287978389.html",
- "Description": "SenseCAP Card Tracker T1000-E Aliexpress"
- },
- {
- "ShortCode": "tracker-t1000-e-amazon",
- "OriginalUrl": "https://www.amazon.com/dp/B0DJ6KGXKB",
- "Description": "SenseCAP Card Tracker T1000-E Amazon"
- },
- {
- "ShortCode": "seeed-sensecap-indicator",
- "OriginalUrl": "https://www.seeedstudio.com/SenseCAP-Indicator-D1L-for-Meshtastic-p-6304.html",
- "Description": "SenseCAP Indicator"
- },
- {
- "ShortCode": "station-g2",
- "OriginalUrl": "https://shop.uniteng.com/product/meshtastic-mesh-device-station-edition/",
- "Description": "Station G2"
- },
- {
- "ShortCode": "rak2560",
- "OriginalUrl": "https://store.rakwireless.com/products/wismesh-meshtastic-solar-repeater?utm_source=website_partner&utm_medium=referral&utm_campaign=meshtastic_rak_collab",
- "Description": "WisMesh Repeater"
- },
- {
- "ShortCode": "heltec-v3",
- "OriginalUrl": "https://heltec.org/project/wifi-lora-32-v3/",
- "Description": "LoRa32 V3"
- },
- {
- "ShortCode": "heltec-wsl-v3",
- "OriginalUrl": "https://heltec.org/project/wireless-stick-lite-v2/",
- "Description": "WSL V3"
- },
- {
- "ShortCode": "heltec-v4",
- "OriginalUrl": "https://heltec.org/project/wifi-lora-32-v4/",
- "Description": "LoRa32 V4"
- },
- {
- "ShortCode": "seeed-xiao-s3",
- "OriginalUrl": "https://www.seeedstudio.com/Wio-SX1262-with-XIAO-ESP32S3-p-5982.html",
- "Description": "XIAO ESP32-S3 + Wio-SX1262 Kit"
- },
- {
- "ShortCode": "tlora-t3s3-epaper",
- "OriginalUrl": "https://lilygo.cc/products/t3-s3-meshtastic",
- "Description": "T3S3"
- },
- {
- "ShortCode": "ht-ct62",
- "OriginalUrl": "https://heltec.org/project/ht-ct62/",
- "Description": "HT-CT62"
- },
- {
- "ShortCode": "seeed_xiao_nrf52840_kit",
- "OriginalUrl": "https://www.seeedstudio.com/XIAO-nRF52840-Wio-SX1262-Kit-for-Meshtastic-p-6400.html",
- "Description": "XIAO nRF52840 & Wio-SX1262 Kit"
- },
- {
- "ShortCode": "seeed_xiao_nrf52840_kit_aliexpress",
- "OriginalUrl": "https://www.aliexpress.us/item/3256808574469954.html",
- "Description": "XIAO nRF52840 & Wio-SX1262 Kit Aliexpress"
- },
- {
- "ShortCode": "thinknode_m1",
- "OriginalUrl": "https://www.elecrow.com/thinknode-m1-meshtastic-lora-signal-transceiver-powered-by-nrf52840-with-154-screen-support-gps.html",
- "Description": "ThinkNode M1"
- },
- {
- "ShortCode": "thinknode_m2",
- "OriginalUrl": "https://www.elecrow.com/thinknode-m2-meshtastic-lora-signal-transceiver-powered-by-esp32-s3-with-1-3-oled-display.html",
- "Description": "ThinkNode M2"
- },
- {
- "ShortCode": "thinknode_m3",
- "OriginalUrl": "https://www.elecrow.com/thinknode-m3-meshtastic-tracker-with-gps-wifi-ble-function-for-indoor-and-outdoor-positioning.html",
- "Description": "ThinkNode M3"
- },
- {
- "ShortCode": "thinknode_m5",
- "OriginalUrl": "https://www.elecrow.com/thinknode-m5-meshtastic-lora-signal-transceiver-esp32-s3-1-54-screen-gps-function.html",
- "Description": "ThinkNode M5"
- },
- {
- "ShortCode": "thinknode_m4",
- "OriginalUrl": "https://www.elecrow.com/thinknode-m4-power-bank-lora-device-with-meshtastic-lora-tracker-function-powered-by-nrf52840.html",
- "Description": "ThinkNode M4"
- },
- {
- "ShortCode": "thinknode_m6",
- "OriginalUrl": "https://www.elecrow.com/thinknode-m6-outdoor-solar-power-for-meshtastic-powered-by-nrf52840-supports-gps.html",
- "Description": "ThinkNode M6"
- },
- {
- "ShortCode": "heltec-mesh-pocket-10000",
- "OriginalUrl": "https://heltec.org/project/meshpocket/",
- "Description": "MeshPocket"
- },
- {
- "ShortCode": "seeed_solar_node",
- "OriginalUrl": "https://www.seeedstudio.com/SenseCAP-Solar-Node-P1-Pro-for-Meshtastic-LoRa-p-6412.html",
- "Description": "SenseCAP Solar Node P1 Pro"
- },
- {
- "ShortCode": "seeed_solar_node_aliexpress",
- "OriginalUrl": "https://www.aliexpress.us/item/3256808731224053.html",
- "Description": "SenseCAP Solar Node P1 Pro Aliexpress"
- },
- {
- "ShortCode": "seeed_solar_node_amazon",
- "OriginalUrl": "https://www.amazon.com/dp/B0FMDHBWX8",
- "Description": "SenseCAP Solar Node P1 Pro Amazon"
- },
- {
- "ShortCode": "elecrow-adv-35-tft",
- "OriginalUrl": "https://www.elecrow.com/crowpanel-advance-3-5-hmi-esp32-ai-display-for-meshtastic-320x240-ips-artificial-intelligent-screen.html",
- "Description": "CrowPanel 3.5"
- },
- {
- "ShortCode": "elecrow-adv1-43-50-70-tft",
- "OriginalUrl": "https://www.elecrow.com/crowpanel-advance-4-3-hmi-ai-screen-for-meshtastic-esp32-800x480-ips-touch-artificial-intelligent-display-2.html",
- "Description": "CrowPanel 4.3"
- },
- {
- "ShortCode": "elecrow-adv-24-28-tft",
- "OriginalUrl": "https://www.elecrow.com/crowpanel-advance-2-4-hmi-ai-display-for-meshtastic-esp32-320x240-ips-artificial-intelligent-touchscreen.html",
- "Description": "CrowPanel 2.4"
- },
- {
- "ShortCode": "elecrow-adv-28-tft",
- "OriginalUrl": "https://www.elecrow.com/crowpanel-advance-2-8-hmi-ai-display-for-meshtastic-esp32-320x240-artificial-ips-intelligent-touchscreen.html",
- "Description": "CrowPanel 2.8"
- },
- {
- "ShortCode": "elecrow-adv1-50-tft",
- "OriginalUrl": "https://www.elecrow.com/crowpanel-advance-5inch-hmi-esp32-ai-display-800x480-ips-artificial-intelligent-touch-screen-support-meshtastic.html",
- "Description": "CrowPanel 5.0"
- },
- {
- "ShortCode": "elecrow-adv1-70-tft",
- "OriginalUrl": "https://www.elecrow.com/crowpanel-advance-7-0-hmi-esp32-ai-display-800x480-artificial-intelligent-ips-touch-screen-for-meshtastic.html",
- "Description": "CrowPanel 7.0"
- },
- {
- "ShortCode": "seeed_wio_tracker_L1",
- "OriginalUrl": "https://www.seeedstudio.com/Wio-Tracker-L1-Pro-p-6454.html",
- "Description": "Wio Tracker L1"
- },
- {
- "ShortCode": "seeed_wio_tracker_L1_aliexpress",
- "OriginalUrl": "https://www.aliexpress.us/item/3256809394050623.html",
- "Description": "Wio Tracker L1 Aliexpress"
- },
- {
- "ShortCode": "seeed_wio_tracker_L1_amazon",
- "OriginalUrl": "https://www.amazon.com/dp/B0FNCS5ST1",
- "Description": "Wio Tracker L1 Amazon"
- },
- {
- "ShortCode": "nano-g2-ultra",
- "OriginalUrl": "https://shop.uniteng.com/product/meshtastic-mesh-device-nano-g2-ultra/",
- "Description": "Nano G2 Ultra"
- },
- {
- "ShortCode": "rak11310",
- "OriginalUrl": "https://store.rakwireless.com/products/wisblock-rp2040-starter-kit-for-meshtastic?utm_source=website_partner&utm_medium=referral&utm_campaign=meshtastic_rak_collab",
- "Description": "RAK11310"
- },
- {
- "ShortCode": "rokland-rak11310",
- "OriginalUrl": "https://store.rakwireless.com/products/wisblock-rp2040-starter-kit-for-meshtastic?utm_source=website_partner&utm_medium=referral&utm_campaign=meshtastic_rak_collab",
- "Description": "Rokland RAKwireless RAK11310 WisBlock RP2040 Core Module"
- },
- {
- "ShortCode": "station-g2-tindie",
- "OriginalUrl": "https://www.tindie.com/products/neilhao/meshtastic-mesh-device-station-g2/",
- "Description": "Station G2 Tindie Listing"
- },
- {
- "ShortCode": "nano-g2-ultra-tindie",
- "OriginalUrl": "https://www.tindie.com/products/neilhao/meshtastic-mesh-device-nano-g2-ultra/",
- "Description": "Nano G2 Ultra Tindie Listing"
- },
- {
- "ShortCode": "t-deck-plus",
- "OriginalUrl": "https://lilygo.cc/products/t-deck-plus-meshtastic",
- "Description": "T-Deck Plus"
- },
- {
- "ShortCode": "rokland-meshtastic-starter-kit",
- "OriginalUrl": "https://store.rokland.com/products/rak-wireless-wisblock-meshtastic-starter-kit",
- "Description": "Rokland Meshtastic Starter Kit"
- },
- {
- "ShortCode": "rokland-t-deck-base",
- "OriginalUrl": "https://store.rokland.com/products/lilygo-t-deck-portable-microcontroller-programmer-lora-915-mhz-h642?variant=41000826372179",
- "Description": "Rokland T-Deck Base"
- },
- {
- "ShortCode": "rokland-t-deck-complete",
- "OriginalUrl": "https://store.rokland.com/products/lilygo-t-deck-portable-microcontroller-programmer-lora-915-mhz-h642?variant=42122265690195",
- "Description": "Rokland T-Deck Complete"
- },
- {
- "ShortCode": "rokland-t-deck-plus",
- "OriginalUrl": "https://store.rokland.com/products/lilygo-t-deck-portable-microcontroller-programmer-lora-915-mhz-h642?variant=42283977834579",
- "Description": "Rokland T-Deck Plus"
- },
- {
- "ShortCode": "rokland-t-echo",
- "OriginalUrl": "https://store.rokland.com/products/lilygo-ttgo-meshtastic-t-echo-white-lora-sx1262-wireless-module-915mhz-nrf52840-gps-for-arduino?ref=8Bb2mUO5i-jKwt",
- "Description": "Rokland T-Echo"
- },
- {
- "ShortCode": "rokland-t-echo-bme280",
- "OriginalUrl": "https://store.rokland.com/products/lilygo-ttgo-meshtastic-t-echo-white-bme280-lora-sx1262-wireless-module-915mhz-nrf52840-gps-rtc-nfc-for-arduino?ref=8Bb2mUO5i-jKwt",
- "Description": "Rokland T-Echo with BME280"
- },
- {
- "ShortCode": "rokland-rak19007",
- "OriginalUrl": "https://store.rokland.com/products/rak-wireless-wisblock-base-board-2nd-gen-rak19007-ver-b-pid-110082",
- "Description": "Rokland RAKwireless RAK19007 WisBlock Base Board 2nd Gen"
- },
- {
- "ShortCode": "hexaspot-rak19007",
- "OriginalUrl": "https://hexaspot.com/collections/rakwireless-wisblock-base/products/rakwireless-rak19007-wisblock-base-board-2nd-gen",
- "Description": "Hexaspot RAKwireless RAK19007 WisBlock Base Board 2nd Gen"
- },
- {
- "ShortCode": "rokland-starter-kit",
- "OriginalUrl": "https://store.rokland.com/products/rak-wireless-wisblock-meshtastic-starter-kit",
- "Description": "Rokland RAKwireless 4631 Starter Kit"
- },
- {
- "ShortCode": "hexaspot-starter-kit",
- "OriginalUrl": "https://hexaspot.com/collections/wisblock-kits/products/wisblock-starter-kit-wisblock-basic-kit",
- "Description": "Hexaspot RAKwireless 4631 Starter Kit"
- },
- {
- "ShortCode": "aliexpress-rak1921",
- "OriginalUrl": "https://www.aliexpress.com/item/3256801470591730.html?utm_source=website_partner&utm_medium=referral&utm_campaign=meshtastic_rak_collab",
- "Description": "RAK1921 OLED Display (AliExpress)"
- },
- {
- "ShortCode": "rak1921",
- "OriginalUrl": "https://store.rakwireless.com/products/rak1921-oled-display-panel?utm_source=website_partner&utm_medium=referral&utm_campaign=meshtastic_rak_collab",
- "Description": "RAK1921 OLED Display (RAK Store)"
- },
- {
- "ShortCode": "rokland-rak1921",
- "OriginalUrl": "https://store.rokland.com/products/rak-wireless-wisblock-oled-display-rak1921-pid-110004",
- "Description": "Rokland RAK1921 WisBlock OLED Display"
- },
- {
- "ShortCode": "muzi-rak1921",
- "OriginalUrl": "https://muzi.works/products/rak-oled-display-ssd1306",
- "Description": "Muzi Works RAK1921 OLED Display SSD1306"
- },
- {
- "ShortCode": "aliexpress-rak14000",
- "OriginalUrl": "https://www.aliexpress.com/item/3256803245280485.html?utm_source=website_partner&utm_medium=referral&utm_campaign=meshtastic_rak_collab",
- "Description": "RAK14000 E-Ink Display (AliExpress)"
- },
- {
- "ShortCode": "rak14000",
- "OriginalUrl": "https://store.rakwireless.com/products/wisblock-epd-module-rak14000?utm_source=website_partner&utm_medium=referral&utm_campaign=meshtastic_rak_collab",
- "Description": "RAK14000 E-Ink Display (RAK Store)"
- },
- {
- "ShortCode": "rokland-rak14000",
- "OriginalUrl": "https://store.rokland.com/products/rak-wireless-wisblock-epd-module-rak14000-pid-110024",
- "Description": "Rokland RAK14000 WisBlock E-Ink Display"
- },
- {
- "ShortCode": "aliexpress-rak12500",
- "OriginalUrl": "https://www.aliexpress.com/item/3256802312416216.html?utm_source=website_partner&utm_medium=referral&utm_campaign=meshtastic_rak_collab",
- "Description": "RAK12500 (AliExpress)"
- },
- {
- "ShortCode": "rak12500",
- "OriginalUrl": "https://store.rakwireless.com/products/wisblock-gnss-location-module-rak12500?utm_source=website_partner&utm_medium=referral&utm_campaign=meshtastic_rak_collab",
- "Description": "RAK12500 (RAK Store)"
- },
- {
- "ShortCode": "rak13300",
- "OriginalUrl": "https://store.rakwireless.com/products/rak13300-wisblock-lpwan?utm_source=website_partner&utm_medium=referral&utm_campaign=meshtastic_rak_collab",
- "Description": "RAK13300 LPWAN Module (RAK Store)"
- },
- {
- "ShortCode": "aliexpress-rak13002",
- "OriginalUrl": "https://www.aliexpress.com/item/3256802904688489.html?utm_source=website_partner&utm_medium=referral&utm_campaign=meshtastic_rak_collab",
- "Description": "RAK13002 IO Module (AliExpress)"
- },
- {
- "ShortCode": "rak13002",
- "OriginalUrl": "https://store.rakwireless.com/products/adapter-module-rak13002?utm_source=website_partner&utm_medium=referral&utm_campaign=meshtastic_rak_collab",
- "Description": "RAK13002 IO Module (RAK Store)"
- },
- {
- "ShortCode": "rokland-rak13002",
- "OriginalUrl": "https://store.rokland.com/products/rak-wireless-rak13002-wisblock-io-adapter-module",
- "Description": "Rokland RAK13002 WisBlock IO Adapter Module"
- },
- {
- "ShortCode": "muzi-rak13002",
- "OriginalUrl": "https://muzi.works/products/rak-io-module",
- "Description": "Muzi Works RAK13002 IO Module"
- },
- {
- "ShortCode": "rak6421",
- "OriginalUrl": "https://store.rakwireless.com/products/meshtastic-raspberry-pi-hat-rak6421?utm_source=website_partner&utm_medium=referral&utm_campaign=meshtastic_rak_collab",
- "Description": "WisMesh Pi Hat RAK6421 (RAK Store)"
- },
- {
- "ShortCode": "aliexpress-rak18001",
- "OriginalUrl": "https://www.aliexpress.com/item/3256802312587439.html?utm_source=website_partner&utm_medium=referral&utm_campaign=meshtastic_rak_collab",
- "Description": "RAK18001 RAK Buzzer (AliExpress)"
- },
- {
- "ShortCode": "rak18001",
- "OriginalUrl": "https://store.rakwireless.com/products/wisblock-buzzer-module-rak18001?utm_source=website_partner&utm_medium=referral&utm_campaign=meshtastic_rak_collab",
- "Description": "RAK18001 RAK Buzzer (RAK Store)"
- },
- {
- "ShortCode": "aliexpress-rak1901",
- "OriginalUrl": "https://www.aliexpress.com/item/3256801444571922.html?utm_source=website_partner&utm_medium=referral&utm_campaign=meshtastic_rak_collab",
- "Description": "RAK1901 Temperature and Humidity Sensor (AliExpress)"
- },
- {
- "ShortCode": "rak1901",
- "OriginalUrl": "https://store.rakwireless.com/products/rak1901-shtc3-temperature-humidity-sensor?utm_source=website_partner&utm_medium=referral&utm_campaign=meshtastic_rak_collab",
- "Description": "RAK1901 Temperature and Humidity Sensor (RAK Store)"
- },
- {
- "ShortCode": "aliexpress-rak1902",
- "OriginalUrl": "https://www.aliexpress.com/item/3256801445721072.html?utm_source=website_partner&utm_medium=referral&utm_campaign=meshtastic_rak_collab",
- "Description": "RAK-1902 Barometric Pressure Sensor (AliExpress)"
- },
- {
- "ShortCode": "rak1902",
- "OriginalUrl": "https://store.rakwireless.com/products/rak1902-kps22hb-barometric-pressure-sensor?utm_source=website_partner&utm_medium=referral&utm_campaign=meshtastic_rak_collab",
- "Description": "RAK-1902 Barometric Pressure Sensor (RAK Store)"
- },
- {
- "ShortCode": "aliexpress-rak1906",
- "OriginalUrl": "https://www.aliexpress.com/item/3256801453209668.html?utm_source=website_partner&utm_medium=referral&utm_campaign=meshtastic_rak_collab",
- "Description": "RAK1906 Environment Sensor (AliExpress)"
- },
- {
- "ShortCode": "rak1906",
- "OriginalUrl": "https://store.rakwireless.com/products/rak1906-bme680-environment-sensor?utm_source=website_partner&utm_medium=referral&utm_campaign=meshtastic_rak_collab",
- "Description": "RAK1906 Environment Sensor (RAK Store)"
- },
- {
- "ShortCode": "aliexpress-rak12002",
- "OriginalUrl": "https://www.aliexpress.com/item/3256803919249064.html?utm_source=website_partner&utm_medium=referral&utm_campaign=meshtastic_rak_collab",
- "Description": "RAK12002 WisBlock RTC Module (AliExpress)"
- },
- {
- "ShortCode": "rak12002",
- "OriginalUrl": "https://store.rakwireless.com/products/rtc-module-rak12002?utm_source=website_partner&utm_medium=referral&utm_campaign=meshtastic_rak_collab",
- "Description": "RAK12002 WisBlock RTC Module (RAK Store)"
- },
- {
- "ShortCode": "rokland-rak12002",
- "OriginalUrl": "https://store.rokland.com/products/rak-wireless-rak12002-rtc-module-micro-crystal-rv-3028-c7-pid-100032",
- "Description": "Rokland RAK12002 RTC Module Micro Crystal RV-3028-C7"
- },
- {
- "ShortCode": "aliexpress-wismesh-pocket-v2",
- "OriginalUrl": "https://www.aliexpress.com/item/3256808087883682.html?utm_source=website_partner&utm_medium=referral&utm_campaign=meshtastic_rak_collab",
- "Description": "WisMesh Pocket V2 (AliExpress)"
- },
- {
- "ShortCode": "rokland-wismesh-pocket-v2",
- "OriginalUrl": "https://store.rokland.com/products/wismesh-pocket",
- "Description": "WisMesh Pocket V2 (Rokland)"
- },
- {
- "ShortCode": "hexaspot-wismesh-pocket-v2",
- "OriginalUrl": "https://hexaspot.com/collections/meshtastic-products/products/wismesh-pocket-v2-ready-to-use-meshtastic-device",
- "Description": "WisMesh Pocket V2 (Hexaspot)"
- },
- {
- "ShortCode": "wismesh-pocket-v2",
- "OriginalUrl": "https://store.rakwireless.com/products/wismesh-pocket?utm_source=website_partner&utm_medium=referral&utm_campaign=meshtastic_rak_collab",
- "Description": "WisMesh Pocket V2 (RAK Store)"
- },
- {
- "ShortCode": "aliexpress-wismesh-pocket-mini",
- "OriginalUrl": "https://www.aliexpress.com/item/3256807998160830.html?utm_source=website_partner&utm_medium=referral&utm_campaign=meshtastic_rak_collab",
- "Description": "WisMesh Pocket Mini (Rokland)"
- },
- {
- "ShortCode": "rokland-wismesh-pocket-mini",
- "OriginalUrl": "https://store.rokland.com/products/rakwireless-wismesh-pocket-mini-all-in-one-meshtastic-handheld-915-mhz-radio-with-lora-antenna",
- "Description": "WisMesh Pocket Mini (Rokland)"
- },
- {
- "ShortCode": "wismesh-pocket-mini",
- "OriginalUrl": "https://store.rakwireless.com/products/wismesh-pocket-mini?utm_source=website_partner&utm_medium=referral&utm_campaign=meshtastic_rak_collab",
- "Description": "WisMesh Pocket Mini (RAK Store)"
- },
- {
- "ShortCode": "aliexpress-rak19026",
- "OriginalUrl": "https://www.aliexpress.com/item/3256808063797462.html?utm_source=website_partner&utm_medium=referral&utm_campaign=meshtastic_rak_collab",
- "Description": "WisMesh Baseboard (AliExpress)"
- },
- {
- "ShortCode": "rokland-rak19026",
- "OriginalUrl": "https://store.rokland.com/products/rakwireless-wismesh-baseboard-rak19026-oled-mounted-gnss-motion-sensor-pid-115125",
- "Description": "WisMesh Baseboard (Rokland)"
- },
- {
- "ShortCode": "rak19026",
- "OriginalUrl": "https://store.rakwireless.com/products/wismesh-baseboard-rak19026?utm_source=website_partner&utm_medium=referral&utm_campaign=meshtastic_rak_collab",
- "Description": "WisMesh Baseboard (RAK Store)"
- },
- {
- "ShortCode": "aliexpress-wismesh-tap",
- "OriginalUrl": "https://www.aliexpress.com/item/3256808097004202.html?utm_source=website_partner&utm_medium=referral&utm_campaign=meshtastic_rak_collab",
- "Description": "RAK WisMesh Tap (AliExpress)"
- },
- {
- "ShortCode": "aliexpress-board-one",
- "OriginalUrl": "https://www.aliexpress.com/item/3256802139951068.html?utm_source=website_partner&utm_medium=referral&utm_campaign=meshtastic_rak_collab",
- "Description": "RAK WisMesh Board ONE (AliExpress)"
- },
- {
- "ShortCode": "board-one",
- "OriginalUrl": "https://store.rakwireless.com/products/wismesh-board-one-meshtastic-node?utm_source=website_partner&utm_medium=referral&utm_campaign=meshtastic_rak_collab",
- "Description": "RAK WisMesh Board ONE (RAK Store)"
- },
- {
- "ShortCode": "rokland-board-one",
- "OriginalUrl": "https://store.rokland.com/products/rakwireless-wismesh-b1-board",
- "Description": "Rokland WisMesh Board ONE (US915 MHz)"
- },
- {
- "ShortCode": "wismesh-repeater",
- "OriginalUrl": "https://store.rakwireless.com/products/wismesh-meshtastic-solar-repeater?utm_source=website_partner&utm_medium=referral&utm_campaign=meshtastic_rak_collab",
- "Description": "WisMesh Repeater (RAK Store)"
- },
- {
- "ShortCode": "aliexpress-wismesh-repeater",
- "OriginalUrl": "https://www.aliexpress.com/item/3256808393658502.html?utm_source=website_partner&utm_medium=referral&utm_campaign=meshtastic_rak_collab",
- "Description": "WisMesh Repeater (AliExpress)"
- },
- {
- "ShortCode": "aliexpress-wismesh-repeater-mini",
- "OriginalUrl": "https://www.aliexpress.com/item/2251832722300348.html?utm_source=website_partner&utm_medium=referral&utm_campaign=meshtastic_rak_collab",
- "Description": "WisMesh Repeater Mini (AliExpress)"
- },
- {
- "ShortCode": "hexaspot-wismesh-repeater-mini",
- "OriginalUrl": "https://hexaspot.com/collections/meshtastic-products/products/wismesh-repeater-mini",
- "Description": "WisMesh Repeater Mini (Hexaspot)"
- },
- {
- "ShortCode": "wismesh-repeater-mini",
- "OriginalUrl": "https://store.rakwireless.com/products/wishmesh-meshtastic-solar-repeater-mini?utm_source=website_partner&utm_medium=referral&utm_campaign=meshtastic_rak_collab",
- "Description": "WisMesh Repeater Mini (RAK Store)"
- },
- {
- "ShortCode": "aliexpress-wismesh-ethernet-gateway",
- "OriginalUrl": "https://www.aliexpress.com/item/3256801470547683.html?utm_source=website_partner&utm_medium=referral&utm_campaign=meshtastic_rak_collab",
- "Description": "WisMesh Ethernet MQTT Gateway (AliExpress)"
- },
- {
- "ShortCode": "wismesh-ethernet-gateway",
- "OriginalUrl": "https://store.rakwireless.com/products/wismesh-ethernet-gateway?utm_source=website_partner&utm_medium=referral&utm_campaign=meshtastic_rak_collab",
- "Description": "WisMesh Ethernet MQTT Gateway (RAK Store)"
- },
- {
- "ShortCode": "aliexpress-wismesh-wifi-gateway",
- "OriginalUrl": "https://www.aliexpress.com/item/3256802139923708.html?utm_source=website_partner&utm_medium=referral&utm_campaign=meshtastic_rak_collab",
- "Description": "WisMesh WiFi MQTT Gateway (AliExpress)"
- },
- {
- "ShortCode": "wismesh-wifi-gateway",
- "OriginalUrl": "https://store.rakwireless.com/products/wismesh-wifi-gateway?utm_source=website_partner&utm_medium=referral&utm_campaign=meshtastic_rak_collab",
- "Description": "WisMesh WiFi MQTT Gateway (RAK Store)"
- },
- {
- "ShortCode": "aliexpress-board-one-pocket",
- "OriginalUrl": "https://www.aliexpress.com/item/3256802139951068.html?utm_source=website_partner&utm_medium=referral&utm_campaign=meshtastic_rak_collab",
- "Description": "RAK WisMesh Board ONE Pocket (AliExpress)"
- },
- {
- "ShortCode": "board-one-pocket",
- "OriginalUrl": "https://store.rakwireless.com/products/wismesh-board-one-pocket-meshtastic-node?utm_source=website_partner&utm_medium=referral&utm_campaign=meshtastic_rak_collab",
- "Description": "RAK WisMesh Board ONE Pocket (RAK Store)"
- },
- {
- "ShortCode": "aliexpress-wismesh-unify-enclosure",
- "OriginalUrl": "https://www.aliexpress.com/item/3256808182747014.html?utm_source=website_partner&utm_medium=referral&utm_campaign=meshtastic_rak_collab",
- "Description": "WisMesh Unify Enclosure (AliExpress)"
- },
- {
- "ShortCode": "wismesh-unify-enclosure",
- "OriginalUrl": "https://store.rakwireless.com/products/wismesh-unify-enclosure?utm_source=website_partner&utm_medium=referral&utm_campaign=meshtastic_rak_collab",
- "Description": "WisMesh Unify Enclosure (RAK Store)"
- },
- {
- "ShortCode": "aliexpress-wismesh-antenna",
- "OriginalUrl": "https://www.aliexpress.com/item/3256808177346156.html?utm_source=website_partner&utm_medium=referral&utm_campaign=meshtastic_rak_collab",
- "Description": "WisMesh Antenna (AliExpress)"
- },
- {
- "ShortCode": "wismesh-antenna",
- "OriginalUrl": "https://store.rakwireless.com/products/wismesh-antenna?utm_source=website_partner&utm_medium=referral&utm_campaign=meshtastic_rak_collab",
- "Description": "WisMesh Antenna (RAK Store)"
- },
- {
- "ShortCode": "muzi-rak4631",
- "OriginalUrl": "https://muzi.works/products/rak-wisblock-meshtastic-starter-kit-us915",
- "Description": "Muzi RAK4631 Starter Kit"
- },
- {
- "ShortCode": "aliexpress-rak19007",
- "OriginalUrl": "https://www.aliexpress.com/item/3256803957557617.html?utm_source=website_partner&utm_medium=referral&utm_campaign=meshtastic_rak_collab",
- "Description": "RAK19007 (AliExpress)"
- },
- {
- "ShortCode": "aliexpress-starter-kit",
- "OriginalUrl": "https://www.aliexpress.com/item/1005006901039995.html?utm_source=website_partner&utm_medium=referral&utm_campaign=meshtastic_rak_collab",
- "Description": "WisMesh RAK4631 Starter Kit (AliExpress)"
- },
- {
- "ShortCode": "rak19003",
- "OriginalUrl": "https://store.rakwireless.com/products/wisblock-base-board-rak19003?utm_source=website_partner&utm_medium=referral&utm_campaign=meshtastic_rak_collab",
- "Description": "RAK19003 (RAK Store)"
- },
- {
- "ShortCode": "aliexpress-rak19003",
- "OriginalUrl": "https://www.aliexpress.com/item/3256803225234826.html?utm_source=website_partner&utm_medium=referral&utm_campaign=meshtastic_rak_collab",
- "Description": "RAK19003 (AliExpress)"
- },
- {
- "ShortCode": "rak19001",
- "OriginalUrl": "https://store.rakwireless.com/products/rak19001-wisblock-dual-io-base-board?utm_source=website_partner&utm_medium=referral&utm_campaign=meshtastic_rak_collab",
- "Description": "RAK19001 WisBlock Dual IO Base Board (RAK Store)"
- },
- {
- "ShortCode": "aliexpress-rak19001",
- "OriginalUrl": "https://www.aliexpress.com/item/3256803962043191.html?utm_source=website_partner&utm_medium=referral&utm_campaign=meshtastic_rak_collab",
- "Description": "RAK19001 WisBlock Dual IO Base Board (AliExpress)"
- },
- {
- "ShortCode": "rokland-19003",
- "OriginalUrl": "https://store.rokland.com/products/rak-wireless-wisblock-mini-base-board-rak19003-ver-b-pid-306024",
- "Description": "Rokland WisBlock Mini Base Board RAK19003 (Ver B)"
- },
- {
- "ShortCode": "hexaspot-19003",
- "OriginalUrl": "https://hexaspot.com/collections/rakwireless-wisblock-base/products/rakwireless-rak19003-wisblock-mini-base-board",
- "Description": "Hexaspot WisBlock Mini Base Board RAK19003 (Ver B)"
- },
- {
- "ShortCode": "rokland-19001",
- "OriginalUrl": "https://store.rokland.com/products/rak-wireless-wisblock-dual-io-base-board-rak19001-pid-110081",
- "Description": "Rokland WisBlock Dual IO Base Board RAK19001"
- },
- {
- "ShortCode": "hexaspot-19001",
- "OriginalUrl": "https://hexaspot.com/collections/rakwireless-wisblock-base/products/rakwireless-rak19001-wisblock-dual-io-base-board",
- "Description": "Hexaspot WisBlock Dual IO Base Board RAK19001"
- },
- {
- "ShortCode": "rokland-4631",
- "OriginalUrl": "https://store.rokland.com/products/rak-wireless-rak4631-nordic-nrf52840-ble-core-module-for-lorawan-with-lora-sx1262",
- "Description": "Rokland RAK4631 Nordic nRF52840 BLE Core Module for LoRaWAN with LoRa SX1262"
- },
- {
- "ShortCode": "hexaspot-4631",
- "OriginalUrl": "https://hexaspot.com/collections/wisblock-kits/products/wisblock-meshtastic-starter-kit-eu868-the-basic-rak4631-meshtastic-kit-for-lora",
- "Description": "Hexaspot RAK4631 Nordic nRF52840 BLE Core Module for LoRaWAN with LoRa SX1262"
- },
- {
- "ShortCode": "aliexpress-rak4631",
- "OriginalUrl": "https://www.aliexpress.us/item/3256801470104151.html?utm_source=website_partner&utm_medium=referral&utm_campaign=meshtastic_rak_collab",
- "Description": "RAK4631 Nordic nRF52840 BLE Core Module (AliExpress)"
- },
- {
- "ShortCode": "rakwireless-4631",
- "OriginalUrl": "https://store.rakwireless.com/products/rak4631-lpwan-node?utm_source=website_partner&utm_medium=referral&utm_campaign=meshtastic_rak_collab",
- "Description": "RAK4631 Nordic nRF52840 BLE Core Module (AliExpress)"
- },
- {
- "ShortCode": "rakwireless-rak11310",
- "OriginalUrl": "https://store.rakwireless.com/products/rak11310-wisblock-lpwan-module?utm_source=website_partner&utm_medium=referral&utm_campaign=meshtastic_rak_collab",
- "Description": "RAK11310 RP2040 Core Module)"
- },
- {
- "ShortCode": "rakwireless-rak3312",
- "OriginalUrl": "https://store.rakwireless.com/products/wisblock-core-module-rak3312-lora-wifi-ble",
- "Description": "RAK3312 ESP32-S3 Core Module"
- },
- {
- "ShortCode": "hexaspot-rak3312",
- "OriginalUrl": "https://hexaspot.com/collections/rakwireless-wisblock/products/espressif-esp32-s3-wifi-ble-dual-core-module-for-lorawan%C2%AE-with-lora-sx1262",
- "Description": "Hexaspot RAK3312 ESP32-S3 Core Module"
- },
- {
- "ShortCode": "rokland-rak3312",
- "OriginalUrl": "https://store.rokland.com/products/rak3312-espressif-esp32-s3-wifi-ble-dual-core-module-for-lorawan-with-lora-sx1262-116208",
- "Description": "Rokland RAK3312 ESP32-S3 Core Module"
- },
- {
- "ShortCode": "rokland-rak3312-starter-kit",
- "OriginalUrl": "https://store.rokland.com/products/wismesh-rak3312-starter-kit-with-meshtastic-firmware",
- "Description": "Rokland RAK3312 ESP32-S3 Starter Kit"
- },
- {
- "ShortCode": "aliexpress-rak11310",
- "OriginalUrl": "https://www.aliexpress.us/item/3256803225175784.html?utm_source=website_partner&utm_medium=referral&utm_campaign=meshtastic_rak_collab",
- "Description": "RAK11310 RP2040 Core Module (AliExpress)"
- },
- {
- "ShortCode": "rokland-1901",
- "OriginalUrl": "https://store.rokland.com/products/rak-wireless-rak1901-temperature-and-humidity-sensor-sensirion-shtc3-pid-100001",
- "Description": "Rokland RAK1901 Temperature and Humidity Sensor"
- },
- {
- "ShortCode": "rokland-1902",
- "OriginalUrl": "https://store.rokland.com/products/rak-wireless-rak1902-barometric-pressure-sensor-stmicroelectronics-lps22hb-100010-2-pack",
- "Description": "Rokland RAK1902 Barometric Pressure Sensor"
- },
- {
- "ShortCode": "rokland-1906",
- "OriginalUrl": "https://store.rokland.com/products/rak-wireless-rak1906-wisblock-environment-sensor-bosch-bme680",
- "Description": "Rokland RAK1906 WisBlock Environment Sensor"
- },
- {
- "ShortCode": "aliexpress-wismesh-tap",
- "OriginalUrl": "https://www.aliexpress.com/item/3256808097004202.html?utm_source=website_partner&utm_medium=referral&utm_campaign=meshtastic_rak_collab",
- "Description": "RAKwireless WisMesh Tap (AliExpress)"
- },
- {
- "ShortCode": "rokland-wismesh-tap",
- "OriginalUrl": "https://store.rokland.com/products/rakwireless-wismesh-tap-touchscreen-915-mhz-handheld-or-mountable-unit-lora-gps",
- "Description": "RAKwireless WisMesh Tap (Rokland)"
- },
- {
- "ShortCode": "rakdap1",
- "OriginalUrl": "https://store.rakwireless.com/products/daplink-tool?utm_source=website_partner&utm_medium=referral&utm_campaign=meshtastic_rak_collab",
- "Description": "RAKwireless RAKDAP1 Debug and Flash Tool"
- },
- {
- "ShortCode": "rokland-heltec-wsl-v3",
- "OriginalUrl": "https://store.rokland.com/collections/heltec-products/products/heltec-wireless-stick-litev3-902-928-mhz/",
- "Description": "Rokland WSL V3"
- },
- {
- "ShortCode": "aliexpress-heltec-wsl-v3",
- "OriginalUrl": "https://www.aliexpress.us/item/3256807466584635.htm",
- "Description": "Aliexpress WSL V3"
- },
- {
- "ShortCode": "rokland-heltec-wireless-tracker",
- "OriginalUrl": "https://store.rokland.com/collections/heltec-products/products/heltec-wireless-tracker-v1-1-wi-fi-lora-bt-gnss/",
- "Description": "Rokland Wireless Tracker"
- },
- {
- "ShortCode": "aliexpress-heltec-wireless-tracker",
- "OriginalUrl": "https://www.aliexpress.us/item/3256805495189423.html",
- "Description": "Aliexpress Wireless Tracker"
- },
- {
- "ShortCode": "aliexpress-heltec-wireless-paper",
- "OriginalUrl": "https://www.aliexpress.us/item/3256805461611876.html",
- "Description": "Aliexpress Wireless Paper"
- },
- {
- "ShortCode": "rokland-heltec-wireless-paper",
- "OriginalUrl": "https://store.rokland.com/collections/heltec-products/products/heltec-wireless-paper-wi-fi-lora-bt/",
- "Description": "Rokland Wireless Paper"
- },
- {
- "ShortCode": "muzi-heltec-mesh-node-t114",
- "OriginalUrl": "https://muzi.works/products/heltec-mesh-node-t114/",
- "Description": "MuziWorks Mesh Node T114"
- },
- {
- "ShortCode": "aliexpress-heltec-mesh-node-t114",
- "OriginalUrl": "https://www.aliexpress.com/item/1005007460963705.html",
- "Description": "Aliexpress Mesh Node T114"
- },
- {
- "ShortCode": "aliexpress-heltec-vision-master-e213",
- "OriginalUrl": "https://www.aliexpress.com/item/1005007209756502.html",
- "Description": "Aliexpress Vision Master E213"
- },
- {
- "ShortCode": "aliexpress-heltec-vision-master-e290",
- "OriginalUrl": "https://www.aliexpress.com/item/1005007234361986.html",
- "Description": "Aliexpress Vision Master E290"
- },
- {
- "ShortCode": "aliexpress-heltec-vision-master-t190",
- "OriginalUrl": "https://www.aliexpress.us/item/3256807135629435.html",
- "Description": "Aliexpress Vision Master T190"
- },
- {
- "ShortCode": "seeed-wio-tracker-l1-oled",
- "OriginalUrl": "https://www.seeedstudio.com/Wio-Tracker-L1-p-6453.html",
- "Description": "Wio Tracker L1 (with OLED)"
- },
- {
- "ShortCode": "seeed-wio-tracker-l1-oled_aliexpress",
- "OriginalUrl": "https://www.aliexpress.us/item/3256809320083189.html",
- "Description": "Wio Tracker L1 (with OLED)"
- },
- {
- "ShortCode": "seeed_wio_tracker_L1_eink",
- "OriginalUrl": "https://www.seeedstudio.com/Wio-Tracker-L1-E-ink-p-6456.html",
- "Description": "Wio Tracker L1 (with E-Ink)"
- },
- {
- "ShortCode": "seeed_wio_tracker_L1_eink_amazon",
- "OriginalUrl": "https://www.amazon.com/dp/B0FJWT5FYW",
- "Description": "Wio Tracker L1 (with E-Ink) Amazon"
- },
- {
- "ShortCode": "seeed-wio-tracker-l1-lite",
- "OriginalUrl": "https://www.seeedstudio.com/Wio-Tracker-L1-Lite-p-6455.html",
- "Description": "Wio Tracker L1 Lite (no display)"
- },
- {
- "ShortCode": "seeed_solar_node_p1",
- "OriginalUrl": "https://www.seeedstudio.com/SenseCAP-Solar-Node-P1-for-Meshtastic-LoRa-p-6425.html",
- "Description": "SenseCAP Solar Node P1"
- },
- {
- "ShortCode": "seeed_solar_node_p1_aliexpress",
- "OriginalUrl": "https://www.aliexpress.us/item/3256808731224053.html",
- "Description": "SenseCAP Solar Node P1 Aliexpress"
- },
- {
- "ShortCode": "android-closed-test",
- "OriginalUrl": "https://forms.gle/3dZCSTQWRbMSHkPd6",
- "Description": "Android Closed Test Form"
- },
- {
- "ShortCode": "t-deck-pro",
- "OriginalUrl": "https://lilygo.cc/products/t-deck-pro-meshtastic",
- "Description": "LilyGo T-Deck Pro"
- },
- {
- "ShortCode": "rak4631_nomadstar_meteor_pro",
- "OriginalUrl": "https://nomadstar.ch/meteor-pro/",
- "Description": "NomadStar Meteor Pro"
- },
- {
- "ShortCode": "muziworks",
- "OriginalUrl": "https://muzi.works/",
- "Description": "muzi WORKS Homepage"
- },
- {
- "ShortCode": "r1-neo",
- "OriginalUrl": "https://muzi.works/products/r1-neo-complete-meshtastic-device",
- "Description": "muzi WORKS R1 Neo"
- },
- {
- "ShortCode": "muzi-base",
- "OriginalUrl": "https://muzi.works/pages/base",
- "Description": "muzi WORKS Base System"
- },
- {
- "ShortCode": "muzi-base-uno",
- "OriginalUrl": "https://muzi.works/products/base-uno",
- "Description": "muzi WORKS Base Uno"
- },
- {
- "ShortCode": "muzi-base-duo",
- "OriginalUrl": "https://muzi.works/products/base-duo",
- "Description": "muzi WORKS Base Duo"
- },
- {
- "ShortCode": "muzi-base-super-io",
- "OriginalUrl": "https://muzi.works/products/super-io",
- "Description": "muzi WORKS Base Super IO"
- },
- {
- "ShortCode": "ttc-tickets",
- "OriginalUrl": "https://www.thethingsconference.com/partner-invitations/recgog1edgosiv3b8",
- "Description": "The Things Conference Tickets"
- },
- {
- "ShortCode": "rokland-atlavox-makers-market",
- "OriginalUrl": "https://store.rokland.com/products/atlavox-beacon-solar-meshtastic-node-w-n-female-antenna",
- "Description": "Rokland Atlavox Makers Market"
- },
- {
- "ShortCode": "rokland-tlora-pager",
- "OriginalUrl": "https://store.rokland.com/products/lilygo-t-lora-pager-us-915-mhz-lora-esp32-s3-handheld-aiot-programmable-development-device-k257-01",
- "Description": "Rokland T-Lora Pager"
- },
- {
- "ShortCode": "tlora-pager",
- "OriginalUrl": "https://lilygo.cc/products/t-lora-pager-meshtastic",
- "Description": "T-Lora Pager"
- },
- {
- "ShortCode": "hexaspot",
- "OriginalUrl": "https://hexaspot.com/collections/meshtastic-products",
- "Description": "Hexaspot Meshtastic Products"
- },
- {
- "ShortCode": "ew26",
- "OriginalUrl": "https://meshtastic.com/ew26",
- "Description": "embeddedworld26 event page"
- },
- {
- "ShortCode": "hexaspot-heltec-v3",
- "OriginalUrl": "https://hexaspot.com/collections/meshtastic-products/products/heltec-wifi-lora-32-v3",
- "Description": "Heltec V3 (Hexaspot)"
- },
- {
- "ShortCode": "hexaspot-heltec-v4",
- "OriginalUrl": "https://hexaspot.com/collections/meshtastic-products/products/heltec-wifi-lora-32-v4",
- "Description": "Heltec V4 (Hexaspot)"
- },
- {
- "ShortCode": "hexaspot-wireless-tracker-v2",
- "OriginalUrl": "https://hexaspot.com/collections/meshtastic-products/products/heltec-wireless-tracker-v2",
- "Description": "Heltec Wireless Tracker V2 (Hexaspot)"
- }
- ]
-}
\ No newline at end of file
diff --git a/core/data/src/androidMain/kotlin/org/meshtastic/core/data/datasource/DeviceLinksJsonDataSourceImpl.kt b/core/data/src/androidMain/kotlin/org/meshtastic/core/data/datasource/DeviceLinksJsonDataSourceImpl.kt
new file mode 100644
index 0000000000..0d96fee777
--- /dev/null
+++ b/core/data/src/androidMain/kotlin/org/meshtastic/core/data/datasource/DeviceLinksJsonDataSourceImpl.kt
@@ -0,0 +1,47 @@
+/*
+ * Copyright (c) 2026 Meshtastic LLC
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+package org.meshtastic.core.data.datasource
+
+import android.app.Application
+import kotlinx.serialization.ExperimentalSerializationApi
+import kotlinx.serialization.json.Json
+import kotlinx.serialization.json.decodeFromStream
+import org.koin.core.annotation.Single
+import org.meshtastic.core.model.NetworkDeviceLink
+import org.meshtastic.core.model.NetworkDeviceLinksResponse
+
+@Single
+class DeviceLinksJsonDataSourceImpl(private val application: Application) : DeviceLinksJsonDataSource {
+
+ // Tolerant parser so additional fields in the bundled snapshot don't break deserialization on older app versions.
+ @OptIn(ExperimentalSerializationApi::class)
+ private val json = Json {
+ ignoreUnknownKeys = true
+ isLenient = true
+ exceptionsWithDebugInfo = false
+ }
+
+ @OptIn(ExperimentalSerializationApi::class)
+ override fun loadDeviceLinksFromJsonAsset(): List<NetworkDeviceLink> =
+ application.assets.open(DEVICE_LINKS_ASSET).use { inputStream ->
+ json.decodeFromStream<NetworkDeviceLinksResponse>(inputStream).links
+ }
+
+ private companion object {
+ const val DEVICE_LINKS_ASSET = "device_links.json"
+ }
+}
diff --git a/core/data/src/androidMain/kotlin/org/meshtastic/core/data/datasource/MshToLinksJsonDataSourceImpl.kt b/core/data/src/androidMain/kotlin/org/meshtastic/core/data/datasource/MshToLinksJsonDataSourceImpl.kt
deleted file mode 100644
index 18d6d71359..0000000000
--- a/core/data/src/androidMain/kotlin/org/meshtastic/core/data/datasource/MshToLinksJsonDataSourceImpl.kt
+++ /dev/null
@@ -1,69 +0,0 @@
-/*
- * Copyright (c) 2026 Meshtastic LLC
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <https://www.gnu.org/licenses/>.
- */
-@file:OptIn(ExperimentalSerializationApi::class)
-
-package org.meshtastic.core.data.datasource
-
-import android.app.Application
-import co.touchlab.kermit.Logger
-import kotlinx.serialization.ExperimentalSerializationApi
-import kotlinx.serialization.json.Json
-import kotlinx.serialization.json.decodeFromStream
-import org.koin.core.annotation.Single
-import org.meshtastic.core.model.MshToMarketplace
-import org.meshtastic.core.model.MshToRoute
-import org.meshtastic.core.model.MshToUrlsFile
-
-@Single
-class MshToLinksJsonDataSourceImpl(private val application: Application) : MshToLinksJsonDataSource {
-
- // Tolerant parser: tolerate extra fields/trailing data so a stale bundled file never crashes the import.
- private val json = Json {
- ignoreUnknownKeys = true
- isLenient = true
- exceptionsWithDebugInfo = false
- }
-
- // The bundled assets are immutable for the install's lifetime, so parse once and reuse — these are read on the
- // node-detail flow's hot path (once per hardware emission).
- private val routes: List<MshToRoute> by lazy {
- runCatching { application.assets.open(URLS_ASSET).use { json.decodeFromStream<MshToUrlsFile>(it).routes } }
- .onFailure { Logger.w(it) { "Unable to load $URLS_ASSET for device links" } }
- .getOrDefault(emptyList())
- }
-
- private val marketplaces: Map<String, MshToMarketplace> by lazy {
- runCatching {
- application.assets.open(MARKETPLACES_ASSET).use {
- json.decodeFromStream<Map<String, MshToMarketplace>>(it)
- }
- }
- .onFailure {
- Logger.w(it) { "Unable to load $MARKETPLACES_ASSET; marketplace links won't be region-filtered" }
- }
- .getOrDefault(emptyMap())
- }
-
- override fun loadRoutes(): List<MshToRoute> = routes
-
- override fun loadMarketplaces(): Map<String, MshToMarketplace> = marketplaces
-
- private companion object {
- const val URLS_ASSET = "urls.json"
- const val MARKETPLACES_ASSET = "marketplaces.json"
- }
-}
diff --git a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/datasource/MshToLinksJsonDataSource.kt b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/datasource/DeviceLinksJsonDataSource.kt
similarity index 58%
rename from core/data/src/commonMain/kotlin/org/meshtastic/core/data/datasource/MshToLinksJsonDataSource.kt
rename to core/data/src/commonMain/kotlin/org/meshtastic/core/data/datasource/DeviceLinksJsonDataSource.kt
index 74a54acb38..06134a77c9 100644
--- a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/datasource/MshToLinksJsonDataSource.kt
+++ b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/datasource/DeviceLinksJsonDataSource.kt
@@ -16,14 +16,9 @@
*/
package org.meshtastic.core.data.datasource
-import org.meshtastic.core.model.MshToMarketplace
-import org.meshtastic.core.model.MshToRoute
+import org.meshtastic.core.model.NetworkDeviceLink
-/** Reads the bundled msh.to link data: `urls.json` (short codes) and `marketplaces.json` (region metadata). */
-interface MshToLinksJsonDataSource {
- /** Routes from the bundled `urls.json`, or empty if missing/malformed. */
- fun loadRoutes(): List<MshToRoute>
-
- /** Marketplace metadata from the bundled `marketplaces.json`, keyed by marketplace identifier. */
- fun loadMarketplaces(): Map<String, MshToMarketplace>
+/** Loads the bundled device-links snapshot (a frozen copy of the `/resource/deviceLinks` API response). */
+interface DeviceLinksJsonDataSource {
+ fun loadDeviceLinksFromJsonAsset(): List<NetworkDeviceLink>
}
diff --git a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/repository/DeviceHardwareRepositoryImpl.kt b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/repository/DeviceHardwareRepositoryImpl.kt
index 0c48d6a173..bb02284b90 100644
--- a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/repository/DeviceHardwareRepositoryImpl.kt
+++ b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/repository/DeviceHardwareRepositoryImpl.kt
@@ -139,8 +139,8 @@ class DeviceHardwareRepositoryImpl(
"DeviceHardwareRepository: network refresh timed out after ${NETWORK_REFRESH_TIMEOUT_MS}ms"
}
} else {
- // Reconcile msh.to links against the freshest catalog (isVendor + orphan pruning). Runs outside
- // the network timeout so a deadline can't cancel it mid-write and leave links half-reconciled.
+ // Refresh msh.to device links from the API after a hardware refresh. Runs outside the hardware
+ // network timeout so that deadline can't cancel it mid-write.
deviceLinkRepository.reconcile()
}
}
diff --git a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/repository/DeviceLinkMatcher.kt b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/repository/DeviceLinkMatcher.kt
deleted file mode 100644
index ce88e99178..0000000000
--- a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/repository/DeviceLinkMatcher.kt
+++ /dev/null
@@ -1,111 +0,0 @@
-/*
- * Copyright (c) 2026 Meshtastic LLC
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <https://www.gnu.org/licenses/>.
- */
-package org.meshtastic.core.data.repository
-
-import org.meshtastic.core.model.DeviceLink
-
-/**
- * Pure matching logic for associating msh.to [DeviceLink]s with a device's `platformioTarget`. Ported from the
- * Meshtastic-Apple `DeviceLinksSection` (multi-tier matching: exact vendor, product variant, marketplace), so the two
- * platforms surface the same links.
- */
-object DeviceLinkMatcher {
-
- /**
- * Links relevant to [target], region-filtered and sorted with vendor/variant links first.
- *
- * @param links all imported links.
- * @param marketplaceKeys known marketplace identifiers (from `marketplaces.json`).
- * @param deviceTargets all known device `platformioTarget`s — used to exclude other devices' links.
- * @param target the viewed device's `platformioTarget`.
- * @param region the user's ISO 3166-1 alpha-2 region for marketplace filtering.
- */
- fun match(
- links: List<DeviceLink>,
- marketplaceKeys: Set<String>,
- deviceTargets: Set<String>,
- target: String,
- region: String,
- ): List<DeviceLink> {
- val variants = buildTargetVariants(target)
- return links
- .filter { link -> matches(link, marketplaceKeys, deviceTargets, target, variants, region) }
- .sortedByDescending { it.isVendor || !isMarketplaceLink(it.shortCode, marketplaceKeys) }
- }
-
- @Suppress("ReturnCount")
- private fun matches(
- link: DeviceLink,
- marketplaceKeys: Set<String>,
- deviceTargets: Set<String>,
- target: String,
- variants: List<String>,
- region: String,
- ): Boolean {
- val code = link.shortCode
-
- // Exact vendor match always wins.
- if (code == target) return true
-
- // A vendor link for a different device is never shown here.
- if (link.isVendor && code != target) return false
-
- // Variant/marketplace-suffix: "<target>-..." or "<target>_...".
- val matchesPrefix = variants.any { code.startsWith("${it}_") || code.startsWith("$it-") }
-
- // Known marketplace prefix: "<marketplace>-<target>" or "<marketplace>_<target>".
- val matchesMarketplacePrefix =
- variants.any { variant -> marketplaceKeys.any { mp -> code == "$mp-$variant" || code == "${mp}_$variant" } }
-
- if (!matchesPrefix && !matchesMarketplacePrefix) return false
-
- // A prefix hit that is itself a different device's target belongs to that device, not this one.
- if (matchesPrefix && code in deviceTargets && code != target) return false
-
- // Region filter: null regions = vendor/variant (always), empty = worldwide, else must include the region.
- val regions = link.regions ?: return true
- if (regions.isEmpty()) return true
- return region in regions
- }
-
- /** True when [code] carries a known marketplace prefix or suffix. */
- fun isMarketplaceLink(code: String, marketplaceKeys: Set<String>): Boolean =
- marketplaceKeyFor(code, marketplaceKeys) != null
-
- /**
- * The marketplace identifier [code] belongs to (as a delimiter-bounded prefix `mp-`/`mp_` or suffix `-mp`/`_mp`),
- * or `null` if none. This is the single source of truth for "is this a marketplace link" — used for import-time
- * region tagging, sort ordering, and UI prominence — so the classifications never disagree. Delimiter bounds avoid
- * mis-tagging codes that merely begin with a marketplace name (e.g. `muziworks` is NOT `muzi`).
- */
- fun marketplaceKeyFor(code: String, marketplaceKeys: Set<String>): String? = marketplaceKeys.firstOrNull { mp ->
- code.startsWith("$mp-") || code.startsWith("${mp}_") || code.endsWith("-$mp") || code.endsWith("_$mp")
- }
-
- /**
- * Alternate target strings for matching. Strips a leading `rak` (e.g. `rak4631` → `4631`) to absorb msh.to naming
- * inconsistencies like `rokland-4631`.
- */
- fun buildTargetVariants(target: String): List<String> {
- val variants = mutableListOf(target)
- if (target.startsWith("rak")) {
- val stripped = target.removePrefix("rak")
- if (stripped.isNotEmpty()) variants.add(stripped)
- }
- return variants
- }
-}
diff --git a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/repository/DeviceLinkRepositoryImpl.kt b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/repository/DeviceLinkRepositoryImpl.kt
index b0cf2d23f4..daa197377a 100644
--- a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/repository/DeviceLinkRepositoryImpl.kt
+++ b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/repository/DeviceLinkRepositoryImpl.kt
@@ -23,92 +23,125 @@ import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
+import kotlinx.coroutines.withContext
+import kotlinx.coroutines.withTimeoutOrNull
import org.koin.core.annotation.Single
+import org.meshtastic.core.common.util.nowMillis
import org.meshtastic.core.common.util.safeCatching
-import org.meshtastic.core.data.datasource.DeviceHardwareLocalDataSource
import org.meshtastic.core.data.datasource.DeviceLinkLocalDataSource
-import org.meshtastic.core.data.datasource.MshToLinksJsonDataSource
+import org.meshtastic.core.data.datasource.DeviceLinksJsonDataSource
import org.meshtastic.core.database.entity.asEntity
import org.meshtastic.core.database.entity.asExternalModel
+import org.meshtastic.core.di.CoroutineDispatchers
import org.meshtastic.core.model.DeviceLink
-import org.meshtastic.core.model.MshToMarketplace
+import org.meshtastic.core.model.NetworkDeviceLink
+import org.meshtastic.core.model.toDeviceLink
+import org.meshtastic.core.model.util.TimeConstants
+import org.meshtastic.core.network.DeviceLinksRemoteDataSource
import org.meshtastic.core.repository.DeviceLinkRepository
+import kotlin.concurrent.Volatile
+/**
+ * Caches the resolved device-links catalog from the Meshtastic API (`/resource/deviceLinks`). The server does all the
+ * classification (type/targets/regions), so the client just seeds from a bundled snapshot, refreshes from the network,
+ * and filters the cache. Mirrors [DeviceHardwareRepositoryImpl]'s seed → single-flight refresh pattern.
+ */
@Single
class DeviceLinkRepositoryImpl(
- private val jsonDataSource: MshToLinksJsonDataSource,
+ private val remoteDataSource: DeviceLinksRemoteDataSource,
+ private val jsonDataSource: DeviceLinksJsonDataSource,
private val localDataSource: DeviceLinkLocalDataSource,
- private val deviceHardwareLocalDataSource: DeviceHardwareLocalDataSource,
+ private val dispatchers: CoroutineDispatchers,
) : DeviceLinkRepository {
- /** Guards the import so concurrent collectors don't run it more than once at a time. */
- private val importMutex = Mutex()
+ /** Serializes seeding and network refreshes so concurrent collectors don't duplicate writes. */
+ private val writeMutex = Mutex()
+
+ @Volatile private var lastRefreshMillis = 0L
override suspend fun ensureImported() {
- if (localDataSource.count() > 0) return
- importMutex.withLock { if (localDataSource.count() == 0) doImport() }
+ ensureSeeded()
}
override suspend fun reconcile() {
- importMutex.withLock { doImport() }
+ writeMutex.withLock {
+ safeCatching {
+ // Bound only the network call by the timeout; the DB write runs after so a deadline can't cancel it
+ // mid-write and leave the cache half-pruned.
+ val remoteLinks =
+ withTimeoutOrNull(NETWORK_REFRESH_TIMEOUT_MS) { remoteDataSource.getDeviceLinks() }
+ if (remoteLinks == null) {
+ Logger.w {
+ "DeviceLinkRepository: network refresh timed out after ${NETWORK_REFRESH_TIMEOUT_MS}ms"
+ }
+ } else {
+ store(remoteLinks)
+ lastRefreshMillis = nowMillis
+ }
+ }
+ .onFailure { e -> Logger.w(e) { "DeviceLinkRepository: network refresh failed" } }
+ }
}
- override suspend fun getLinksForTarget(platformioTarget: String, regionCode: String): List<DeviceLink> {
- if (platformioTarget.isBlank()) return emptyList()
- ensureImported()
- val links = localDataSource.getAll().map { it.asExternalModel() }
- val marketplaceKeys = jsonDataSource.loadMarketplaces().keys
- val deviceTargets = deviceHardwareLocalDataSource.getAllTargets().toSet()
- return DeviceLinkMatcher.match(
- links = links,
- marketplaceKeys = marketplaceKeys,
- deviceTargets = deviceTargets,
- target = platformioTarget,
- region = regionCode,
- )
- }
+ override suspend fun getLinksForTarget(platformioTarget: String, regionCode: String): List<DeviceLink> =
+ withContext(dispatchers.io) {
+ if (platformioTarget.isBlank()) return@withContext emptyList()
+ ensureSeeded()
+ // Deliberately non-blocking: the node-detail flow must stay snappy. Freshness arrives via reconcile(),
+ // triggered by the device-hardware refresh that resolves this device's hardware in the first place.
+ localDataSource
+ .getAll()
+ .map { it.asExternalModel() }
+ .filter { link -> platformioTarget in link.targets.orEmpty() }
+ .filter { link ->
+ val regions = link.regions
+ regions.isNullOrEmpty() || regionCode in regions
+ }
+ .sortedByDescending { it.isVendor }
+ }
override fun observeAllLinks(): Flow<List<DeviceLink>> = flow {
- ensureImported()
+ ensureSeeded()
+ refreshIfStale()
emitAll(localDataSource.observeAll().map { entities -> entities.map { it.asExternalModel() } })
}
- /** Loads bundled `urls.json`, classifies each short code, upserts, and prunes orphans. Mirrors Apple's import. */
- private suspend fun doImport() {
- safeCatching {
- val routes = jsonDataSource.loadRoutes()
- if (routes.isEmpty()) {
- Logger.w { "DeviceLinkRepository: no routes in bundled urls.json; skipping import" }
- return@safeCatching
+ /** Seeds the table from the bundled snapshot if empty (fresh install, data clear, radio switch). */
+ private suspend fun ensureSeeded() {
+ if (localDataSource.count() > 0) return
+ writeMutex.withLock {
+ if (localDataSource.count() == 0) {
+ safeCatching { store(jsonDataSource.loadDeviceLinksFromJsonAsset()) }
+ .onFailure { e -> Logger.w(e) { "DeviceLinkRepository: failed to seed from bundled JSON" } }
}
- val marketplaces = jsonDataSource.loadMarketplaces()
- val deviceTargets = deviceHardwareLocalDataSource.getAllTargets().toSet()
-
- val links =
- routes.map { route ->
- val isVendor = route.shortCode in deviceTargets
- DeviceLink(
- shortCode = route.shortCode,
- originalUrl = route.originalUrl,
- description = route.description,
- isVendor = isVendor,
- regions = if (isVendor) null else marketplaceRegions(route.shortCode, marketplaces),
- )
- }
-
- localDataSource.upsertAll(links.map { it.asEntity() })
- localDataSource.deleteNotIn(links.map { it.shortCode })
- Logger.i { "DeviceLinkRepository: imported ${links.size} msh.to links" }
}
- .onFailure { Logger.w(it) { "DeviceLinkRepository: device links import failed" } }
+ }
+
+ /** Best-effort network refresh, gated by [CACHE_EXPIRATION_TIME_MS]. */
+ private suspend fun refreshIfStale() {
+ if (nowMillis - lastRefreshMillis > CACHE_EXPIRATION_TIME_MS) reconcile()
}
/**
- * Shipping regions for a marketplace short code, or null when it is not a marketplace link. Uses the same
- * delimiter-aware classifier as the matcher/UI so a code's classification (vendor/variant vs marketplace) is
- * consistent everywhere — independent of the `match` hint in `marketplaces.json`, which is unreliable in practice
- * (e.g. AliExpress is declared `suffix` yet most codes use the `aliexpress-<target>` prefix form).
+ * Maps resolved API links to the cached domain model, upserts them, and prunes short codes that no longer exist.
+ * Internal links (GitHub, YouTube, …) are dropped — they never belong to a device's purchase section. An empty list
+ * is ignored rather than wiping the cache on a bad response.
*/
- private fun marketplaceRegions(code: String, marketplaces: Map<String, MshToMarketplace>): List<String>? =
- DeviceLinkMatcher.marketplaceKeyFor(code, marketplaces.keys)?.let { marketplaces.getValue(it).regions }
+ private suspend fun store(networkLinks: List<NetworkDeviceLink>) {
+ val links = networkLinks.filter { it.type != NetworkDeviceLink.TYPE_INTERNAL }.map { it.toDeviceLink() }
+ if (links.isEmpty()) {
+ Logger.w { "DeviceLinkRepository: no device links to store; leaving cache untouched" }
+ return
+ }
+ localDataSource.upsertAll(links.map { it.asEntity() })
+ localDataSource.deleteNotIn(links.map { it.shortCode })
+ Logger.i { "DeviceLinkRepository: stored ${links.size} device links" }
+ }
+
+ private companion object {
+ private val CACHE_EXPIRATION_TIME_MS = TimeConstants.ONE_DAY.inWholeMilliseconds
+
+ /** Maximum time to wait for the remote API before falling back to cached/bundled data. */
+ private const val NETWORK_REFRESH_TIMEOUT_MS = 5_000L
+ }
}
diff --git a/core/data/src/commonTest/kotlin/org/meshtastic/core/data/repository/DeviceLinkMatcherTest.kt b/core/data/src/commonTest/kotlin/org/meshtastic/core/data/repository/DeviceLinkMatcherTest.kt
deleted file mode 100644
index b9b69edd2d..0000000000
--- a/core/data/src/commonTest/kotlin/org/meshtastic/core/data/repository/DeviceLinkMatcherTest.kt
+++ /dev/null
@@ -1,147 +0,0 @@
-/*
- * Copyright (c) 2026 Meshtastic LLC
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <https://www.gnu.org/licenses/>.
- */
-package org.meshtastic.core.data.repository
-
-import org.meshtastic.core.model.DeviceLink
-import kotlin.test.Test
-import kotlin.test.assertEquals
-import kotlin.test.assertFalse
-import kotlin.test.assertNull
-import kotlin.test.assertTrue
-
-/**
- * Tests for [DeviceLinkMatcher], grounded in the acceptance scenarios of the Meshtastic-Apple `010-device-mshto-links`
- * spec. Mirrors the as-built `DeviceLinksSection` matching (platformioTarget, not hwModelSlug).
- */
-class DeviceLinkMatcherTest {
-
- private val marketplaceKeys = setOf("rokland", "hexaspot", "aliexpress", "amazon", "tindie", "muzi")
-
- private val deviceTargets =
- setOf("rak4631", "heltec-v3", "seeed_solar_node", "tbeam", "rak4631_nomadstar_meteor_pro")
-
- private fun link(shortCode: String, isVendor: Boolean = false, regions: List<String>? = null) = DeviceLink(
- shortCode = shortCode,
- originalUrl = "https://example.com/$shortCode",
- isVendor = isVendor,
- regions = regions,
- )
-
- private fun match(links: List<DeviceLink>, target: String, region: String = "US") =
- DeviceLinkMatcher.match(links, marketplaceKeys, deviceTargets, target, region).map { it.shortCode }
-
- @Test
- fun exactVendorMatchIsIncluded() {
- val result = match(listOf(link("heltec-v3", isVendor = true)), target = "heltec-v3")
- assertEquals(listOf("heltec-v3"), result)
- }
-
- @Test
- fun foreignVendorLinkIsExcluded() {
- // Scenario 5: rak4631_nomadstar_meteor_pro (a different device's target) must NOT show for rak4631.
- val result =
- match(
- listOf(link("rak4631", isVendor = true), link("rak4631_nomadstar_meteor_pro", isVendor = true)),
- target = "rak4631",
- )
- assertEquals(listOf("rak4631"), result)
- }
-
- @Test
- fun productVariantIsIncludedAndProminent() {
- val result = match(listOf(link("rak4631_epaper")), target = "rak4631")
- assertEquals(listOf("rak4631_epaper"), result)
- }
-
- @Test
- fun marketplaceLinkIsRegionFiltered() {
- val links = listOf(link("rokland-rak4631", regions = listOf("US", "CA")))
- assertEquals(listOf("rokland-rak4631"), match(links, target = "rak4631", region = "US"))
- assertEquals(emptyList(), match(links, target = "rak4631", region = "DE"))
- }
-
- @Test
- fun rakPrefixIsStrippedForMarketplaceVariantMatch() {
- // "rokland-4631" should match device "rak4631" via the rak-stripped variant "4631".
- val result = match(listOf(link("rokland-4631", regions = listOf("US"))), target = "rak4631", region = "US")
- assertEquals(listOf("rokland-4631"), result)
- }
-
- @Test
- fun worldwideMarketplaceShowsRegardlessOfRegion() {
- val links = listOf(link("rak4631_aliexpress", regions = emptyList()))
- assertEquals(listOf("rak4631_aliexpress"), match(links, target = "rak4631", region = "ZZ"))
- }
-
- @Test
- fun unrelatedLinksProduceEmptyResult() {
- val links =
- listOf(
- link("github"),
- link("heltec-v3", isVendor = true),
- link("rokland-heltec-v3", regions = listOf("US")),
- )
- assertEquals(emptyList(), match(links, target = "tbeam"))
- }
-
- @Test
- fun anotherDevicesTargetIsNotMatchedAsVariant() {
- // "rak4631_nomadstar_meteor_pro" prefix-matches "rak4631_" but is itself a device target → excluded.
- val result = match(listOf(link("rak4631_nomadstar_meteor_pro")), target = "rak4631")
- assertEquals(emptyList(), result)
- }
-
- @Test
- fun vendorAndVariantSortBeforeMarketplace() {
- val links =
- listOf(
- link("rak4631_aliexpress", regions = emptyList()),
- link("rak4631", isVendor = true),
- link("rokland-rak4631", regions = listOf("US")),
- link("rak4631_epaper"),
- )
- val result = match(links, target = "rak4631", region = "US")
- // Vendor + variant first (order among them preserved from input), marketplace links after.
- assertEquals(listOf("rak4631", "rak4631_epaper", "rak4631_aliexpress", "rokland-rak4631"), result)
- }
-
- @Test
- fun buildTargetVariantsStripsRakPrefix() {
- assertEquals(listOf("rak4631", "4631"), DeviceLinkMatcher.buildTargetVariants("rak4631"))
- assertEquals(listOf("heltec-v3"), DeviceLinkMatcher.buildTargetVariants("heltec-v3"))
- // Bare "rak" strips to empty and is not added.
- assertEquals(listOf("rak"), DeviceLinkMatcher.buildTargetVariants("rak"))
- }
-
- @Test
- fun isMarketplaceLinkDetectsPrefixAndSuffix() {
- assertTrue(DeviceLinkMatcher.isMarketplaceLink("rokland-rak4631", marketplaceKeys))
- assertTrue(DeviceLinkMatcher.isMarketplaceLink("heltec-v3_aliexpress", marketplaceKeys))
- assertFalse(DeviceLinkMatcher.isMarketplaceLink("heltec-v3", marketplaceKeys))
- }
-
- @Test
- fun marketplaceKeyForUsesDelimiterBounds() {
- // Both prefix and suffix forms resolve to their marketplace...
- assertEquals("rokland", DeviceLinkMatcher.marketplaceKeyFor("rokland-rak4631", marketplaceKeys))
- assertEquals("aliexpress", DeviceLinkMatcher.marketplaceKeyFor("aliexpress-rak1921", marketplaceKeys))
- assertEquals("aliexpress", DeviceLinkMatcher.marketplaceKeyFor("rak4631_aliexpress", marketplaceKeys))
- // ...but a code that merely begins with a marketplace name is NOT that marketplace.
- assertNull(DeviceLinkMatcher.marketplaceKeyFor("muziworks", marketplaceKeys))
- assertNull(DeviceLinkMatcher.marketplaceKeyFor("heltec-v3", marketplaceKeys))
- }
-}
diff --git a/core/data/src/jvmTest/kotlin/org/meshtastic/core/data/repository/DeviceLinkRepositoryImplTest.kt b/core/data/src/jvmTest/kotlin/org/meshtastic/core/data/repository/DeviceLinkRepositoryImplTest.kt
index a76c648f2c..14d2f5274d 100644
--- a/core/data/src/jvmTest/kotlin/org/meshtastic/core/data/repository/DeviceLinkRepositoryImplTest.kt
+++ b/core/data/src/jvmTest/kotlin/org/meshtastic/core/data/repository/DeviceLinkRepositoryImplTest.kt
@@ -18,145 +18,168 @@ package org.meshtastic.core.data.repository
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.runTest
-import org.meshtastic.core.data.datasource.DeviceHardwareLocalDataSource
import org.meshtastic.core.data.datasource.DeviceLinkLocalDataSource
-import org.meshtastic.core.data.datasource.MshToLinksJsonDataSource
+import org.meshtastic.core.data.datasource.DeviceLinksJsonDataSource
import org.meshtastic.core.di.CoroutineDispatchers
-import org.meshtastic.core.model.MshToMarketplace
-import org.meshtastic.core.model.MshToRoute
import org.meshtastic.core.model.NetworkDeviceHardware
+import org.meshtastic.core.model.NetworkDeviceLink
+import org.meshtastic.core.model.NetworkDeviceLinksResponse
+import org.meshtastic.core.model.NetworkFirmwareReleases
+import org.meshtastic.core.network.DeviceLinksRemoteDataSource
+import org.meshtastic.core.network.service.ApiService
import org.meshtastic.core.testing.FakeDatabaseProvider
import kotlin.test.AfterTest
import kotlin.test.BeforeTest
import kotlin.test.Test
import kotlin.test.assertEquals
-import kotlin.test.assertNull
import kotlin.test.assertTrue
class DeviceLinkRepositoryImplTest {
- private class FakeMshToLinksJsonDataSource(
- var routes: List<MshToRoute>,
- var marketplaces: Map<String, MshToMarketplace>,
- ) : MshToLinksJsonDataSource {
- override fun loadRoutes(): List<MshToRoute> = routes
+ /** Only [getDeviceLinks] is exercised; the other endpoints are never called by the link repository. */
+ private class FakeApiService(var response: NetworkDeviceLinksResponse) : ApiService {
+ override suspend fun getDeviceHardware(): List<NetworkDeviceHardware> = error("unused")
- override fun loadMarketplaces(): Map<String, MshToMarketplace> = marketplaces
+ override suspend fun getDeviceLinks(): NetworkDeviceLinksResponse = response
+
+ override suspend fun getFirmwareReleases(): NetworkFirmwareReleases = error("unused")
+ }
+
+ private class FakeDeviceLinksJsonDataSource(var links: List<NetworkDeviceLink>) : DeviceLinksJsonDataSource {
+ override fun loadDeviceLinksFromJsonAsset(): List<NetworkDeviceLink> = links
}
private val dispatcher = UnconfinedTestDispatcher()
private val dispatchers = CoroutineDispatchers(main = dispatcher, io = dispatcher, default = dispatcher)
private lateinit var dbProvider: FakeDatabaseProvider
- private lateinit var linkLocal: DeviceLinkLocalDataSource
- private lateinit var hardwareLocal: DeviceHardwareLocalDataSource
- private lateinit var json: FakeMshToLinksJsonDataSource
+ private lateinit var local: DeviceLinkLocalDataSource
+ private lateinit var api: FakeApiService
+ private lateinit var seed: FakeDeviceLinksJsonDataSource
private lateinit var repository: DeviceLinkRepositoryImpl
- private val marketplaces =
- mapOf(
- "rokland" to MshToMarketplace(regions = listOf("US"), match = "prefix"),
- "aliexpress" to MshToMarketplace(regions = emptyList(), match = "suffix"),
- )
-
- private fun route(shortCode: String) =
- MshToRoute(shortCode = shortCode, originalUrl = "https://example.com/$shortCode", description = shortCode)
+ private fun link(
+ shortCode: String,
+ type: String = NetworkDeviceLink.TYPE_VENDOR,
+ targets: List<String>? = null,
+ regions: List<String>? = null,
+ ) = NetworkDeviceLink(
+ shortCode = shortCode,
+ url = "https://msh.to/$shortCode",
+ description = shortCode,
+ type = type,
+ targets = targets,
+ regions = regions,
+ )
@BeforeTest
fun setup() {
dbProvider = FakeDatabaseProvider()
- linkLocal = DeviceLinkLocalDataSource(dbProvider, dispatchers)
- hardwareLocal = DeviceHardwareLocalDataSource(dbProvider, dispatchers)
- json =
- FakeMshToLinksJsonDataSource(
- routes =
- listOf(route("rak4631"), route("rokland-rak4631"), route("rak4631_aliexpress"), route("github")),
- marketplaces = marketplaces,
+ local = DeviceLinkLocalDataSource(dbProvider, dispatchers)
+ api = FakeApiService(NetworkDeviceLinksResponse())
+ seed = FakeDeviceLinksJsonDataSource(emptyList())
+ repository =
+ DeviceLinkRepositoryImpl(
+ remoteDataSource = DeviceLinksRemoteDataSource(api, dispatchers),
+ jsonDataSource = seed,
+ localDataSource = local,
+ dispatchers = dispatchers,
)
- repository = DeviceLinkRepositoryImpl(json, linkLocal, hardwareLocal)
}
@AfterTest fun tearDown() = dbProvider.close()
- private suspend fun seedDeviceTargets(vararg targets: String) {
- hardwareLocal.insertAllDeviceHardware(
- targets.mapIndexed { i, t -> NetworkDeviceHardware(hwModel = i + 1, platformioTarget = t) },
- )
- }
-
@Test
- fun importClassifiesVendorAndMarketplaceLinks() = runTest(dispatcher) {
- seedDeviceTargets("rak4631", "heltec-v3")
- repository.reconcile()
-
- val byCode = linkLocal.getAll().associateBy { it.shortCode }
- assertEquals(4, byCode.size)
-
- // rak4631 is a known device target → vendor, no regions.
- assertTrue(byCode.getValue("rak4631").isVendor)
- assertNull(byCode.getValue("rak4631").regions)
+ fun seedsFromBundledJsonWhenEmptyAndDropsInternalLinks() = runTest(dispatcher) {
+ seed.links =
+ listOf(
+ link("rak4631", targets = listOf("rak4631")),
+ link("github", type = NetworkDeviceLink.TYPE_INTERNAL),
+ )
+ repository.ensureImported()
- // rokland-rak4631 → prefix marketplace, region-tagged.
- assertTrue(!byCode.getValue("rokland-rak4631").isVendor)
- assertEquals(listOf("US"), byCode.getValue("rokland-rak4631").regions)
+ assertEquals(setOf("rak4631"), local.getAll().map { it.shortCode }.toSet())
+ }
- // rak4631_aliexpress → suffix marketplace, worldwide (empty regions).
- assertEquals(emptyList(), byCode.getValue("rak4631_aliexpress").regions)
+ @Test
+ fun ensureImportedSeedsOnlyWhenEmpty() = runTest(dispatcher) {
+ seed.links = listOf(link("rak4631", targets = listOf("rak4631")))
+ repository.ensureImported()
+ assertEquals(1, local.count())
- // github → neither vendor nor marketplace, null regions.
- assertTrue(!byCode.getValue("github").isVendor)
- assertNull(byCode.getValue("github").regions)
+ // A larger snapshot must NOT re-seed once the table is populated.
+ seed.links = seed.links + link("heltec-v3", targets = listOf("heltec-v3"))
+ repository.ensureImported()
+ assertEquals(1, local.count())
}
@Test
- fun reconcilePrunesOrphanedShortCodes() = runTest(dispatcher) {
- seedDeviceTargets("rak4631")
+ fun getLinksForTargetFiltersByTargetAndRegionVendorFirst() = runTest(dispatcher) {
+ api.response =
+ NetworkDeviceLinksResponse(
+ links =
+ listOf(
+ link(
+ "rokland-rak4631",
+ type = NetworkDeviceLink.TYPE_MARKETPLACE,
+ targets = listOf("rak4631"),
+ regions = listOf("US"),
+ ),
+ link("rak4631", targets = listOf("rak4631")),
+ link("heltec-v3", targets = listOf("heltec-v3")),
+ link(
+ "de-only",
+ type = NetworkDeviceLink.TYPE_MARKETPLACE,
+ targets = listOf("rak4631"),
+ regions = listOf("DE"),
+ ),
+ ),
+ )
repository.reconcile()
- assertEquals(4, linkLocal.count())
- // Drop "github" from the bundled file and reconcile again.
- json.routes = json.routes.filterNot { it.shortCode == "github" }
- repository.reconcile()
+ val links = repository.getLinksForTarget("rak4631", regionCode = "US")
- val codes = linkLocal.getAll().map { it.shortCode }.toSet()
- assertEquals(setOf("rak4631", "rokland-rak4631", "rak4631_aliexpress"), codes)
+ // de-only filtered by region; heltec-v3 filtered by target; vendor sorted ahead of marketplace.
+ assertEquals(listOf("rak4631", "rokland-rak4631"), links.map { it.shortCode })
+ assertTrue(links.first().isVendor)
}
@Test
- fun aliexpressPrefixFormIsClassifiedAsWorldwideMarketplace() = runTest(dispatcher) {
- // AliExpress is declared match="suffix" yet most bundled codes use the `aliexpress-<target>` prefix form;
- // import must still classify it as a (worldwide) marketplace link, not a null-region variant.
- json.routes = listOf(route("rak4631"), route("aliexpress-rak4631"))
- seedDeviceTargets("rak4631")
+ fun worldwideLinksShowRegardlessOfRegion() = runTest(dispatcher) {
+ api.response =
+ NetworkDeviceLinksResponse(
+ links =
+ listOf(
+ link("ww", type = NetworkDeviceLink.TYPE_MARKETPLACE, targets = listOf("t"), regions = null),
+ ),
+ )
repository.reconcile()
- assertEquals(emptyList(), linkLocal.getAll().single { it.shortCode == "aliexpress-rak4631" }.regions)
+ assertEquals(listOf("ww"), repository.getLinksForTarget("t", regionCode = "ZZ").map { it.shortCode })
}
@Test
- fun bareMarketplaceNamePrefixIsNotMistagged() = runTest(dispatcher) {
- // "muziworks" merely begins with "muzi" — delimiter bounds must keep it from inheriting muzi's regions.
- json =
- FakeMshToLinksJsonDataSource(
- routes = listOf(route("muziworks")),
- marketplaces = mapOf("muzi" to MshToMarketplace(regions = listOf("US"), match = "prefix")),
+ fun reconcilePrunesShortCodesNoLongerInCatalog() = runTest(dispatcher) {
+ api.response =
+ NetworkDeviceLinksResponse(
+ links = listOf(link("a", targets = listOf("t")), link("b", targets = listOf("t"))),
)
- repository = DeviceLinkRepositoryImpl(json, linkLocal, hardwareLocal)
- seedDeviceTargets("rak4631")
repository.reconcile()
+ assertEquals(2, local.count())
- assertNull(linkLocal.getAll().single().regions)
+ api.response = NetworkDeviceLinksResponse(links = listOf(link("a", targets = listOf("t"))))
+ repository.reconcile()
+ assertEquals(setOf("a"), local.getAll().map { it.shortCode }.toSet())
}
@Test
- fun ensureImportedSeedsOnlyWhenEmpty() = runTest(dispatcher) {
- seedDeviceTargets("rak4631")
- repository.ensureImported()
- assertEquals(4, linkLocal.count())
+ fun emptyResponseLeavesCacheUntouched() = runTest(dispatcher) {
+ api.response = NetworkDeviceLinksResponse(links = listOf(link("a", targets = listOf("t"))))
+ repository.reconcile()
+ assertEquals(1, local.count())
- // A second ensureImported with a larger bundled file must NOT re-import (table already populated).
- json.routes = json.routes + route("new-code")
- repository.ensureImported()
- assertEquals(4, linkLocal.count())
+ api.response = NetworkDeviceLinksResponse(links = emptyList())
+ repository.reconcile()
+ assertEquals(1, local.count())
}
}
diff --git a/core/database/schemas/org.meshtastic.core.database.MeshtasticDatabase/43.json b/core/database/schemas/org.meshtastic.core.database.MeshtasticDatabase/43.json
new file mode 100644
index 0000000000..a2e94c0ae3
--- /dev/null
+++ b/core/database/schemas/org.meshtastic.core.database.MeshtasticDatabase/43.json
@@ -0,0 +1,1581 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 43,
+ "identityHash": "c58b8f2f228a2a98ef9faf320b388373",
+ "entities": [
+ {
+ "tableName": "my_node",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`myNodeNum` INTEGER NOT NULL, `model` TEXT, `firmwareVersion` TEXT, `couldUpdate` INTEGER NOT NULL, `shouldUpdate` INTEGER NOT NULL, `currentPacketId` INTEGER NOT NULL, `messageTimeoutMsec` INTEGER NOT NULL, `minAppVersion` INTEGER NOT NULL, `maxChannels` INTEGER NOT NULL, `hasWifi` INTEGER NOT NULL, `deviceId` TEXT, `pioEnv` TEXT, PRIMARY KEY(`myNodeNum`))",
+ "fields": [
+ {
+ "fieldPath": "myNodeNum",
+ "columnName": "myNodeNum",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "model",
+ "columnName": "model",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "firmwareVersion",
+ "columnName": "firmwareVersion",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "couldUpdate",
+ "columnName": "couldUpdate",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "shouldUpdate",
+ "columnName": "shouldUpdate",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "currentPacketId",
+ "columnName": "currentPacketId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "messageTimeoutMsec",
+ "columnName": "messageTimeoutMsec",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "minAppVersion",
+ "columnName": "minAppVersion",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "maxChannels",
+ "columnName": "maxChannels",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "hasWifi",
+ "columnName": "hasWifi",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "deviceId",
+ "columnName": "deviceId",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "pioEnv",
+ "columnName": "pioEnv",
+ "affinity": "TEXT"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "myNodeNum"
+ ]
+ }
+ },
+ {
+ "tableName": "nodes",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`num` INTEGER NOT NULL, `user` BLOB NOT NULL, `long_name` TEXT, `short_name` TEXT, `position` BLOB NOT NULL, `latitude` REAL NOT NULL, `longitude` REAL NOT NULL, `snr` REAL NOT NULL, `rssi` INTEGER NOT NULL, `last_heard` INTEGER NOT NULL, `device_metrics` BLOB NOT NULL, `channel` INTEGER NOT NULL, `via_mqtt` INTEGER NOT NULL, `hops_away` INTEGER NOT NULL, `is_favorite` INTEGER NOT NULL, `is_ignored` INTEGER NOT NULL DEFAULT 0, `is_muted` INTEGER NOT NULL DEFAULT 0, `environment_metrics` BLOB NOT NULL, `power_metrics` BLOB NOT NULL, `air_quality_metrics` BLOB NOT NULL DEFAULT x'', `paxcounter` BLOB NOT NULL, `public_key` BLOB, `notes` TEXT NOT NULL DEFAULT '', `manually_verified` INTEGER NOT NULL DEFAULT 0, `node_status` TEXT, `last_transport` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`num`))",
+ "fields": [
+ {
+ "fieldPath": "num",
+ "columnName": "num",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "user",
+ "columnName": "user",
+ "affinity": "BLOB",
+ "notNull": true
+ },
+ {
+ "fieldPath": "longName",
+ "columnName": "long_name",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "shortName",
+ "columnName": "short_name",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "position",
+ "columnName": "position",
+ "affinity": "BLOB",
+ "notNull": true
+ },
+ {
+ "fieldPath": "latitude",
+ "columnName": "latitude",
+ "affinity": "REAL",
+ "notNull": true
+ },
+ {
+ "fieldPath": "longitude",
+ "columnName": "longitude",
+ "affinity": "REAL",
+ "notNull": true
+ },
+ {
+ "fieldPath": "snr",
+ "columnName": "snr",
+ "affinity": "REAL",
+ "notNull": true
+ },
+ {
+ "fieldPath": "rssi",
+ "columnName": "rssi",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "lastHeard",
+ "columnName": "last_heard",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "deviceTelemetry",
+ "columnName": "device_metrics",
+ "affinity": "BLOB",
+ "notNull": true
+ },
+ {
+ "fieldPath": "channel",
+ "columnName": "channel",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "viaMqtt",
+ "columnName": "via_mqtt",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "hopsAway",
+ "columnName": "hops_away",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isFavorite",
+ "columnName": "is_favorite",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isIgnored",
+ "columnName": "is_ignored",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "isMuted",
+ "columnName": "is_muted",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "environmentTelemetry",
+ "columnName": "environment_metrics",
+ "affinity": "BLOB",
+ "notNull": true
+ },
+ {
+ "fieldPath": "powerTelemetry",
+ "columnName": "power_metrics",
+ "affinity": "BLOB",
+ "notNull": true
+ },
+ {
+ "fieldPath": "airQualityTelemetry",
+ "columnName": "air_quality_metrics",
+ "affinity": "BLOB",
+ "notNull": true,
+ "defaultValue": "x''"
+ },
+ {
+ "fieldPath": "paxcounter",
+ "columnName": "paxcounter",
+ "affinity": "BLOB",
+ "notNull": true
+ },
+ {
+ "fieldPath": "publicKey",
+ "columnName": "public_key",
+ "affinity": "BLOB"
+ },
+ {
+ "fieldPath": "notes",
+ "columnName": "notes",
+ "affinity": "TEXT",
+ "notNull": true,
+ "defaultValue": "''"
+ },
+ {
+ "fieldPath": "manuallyVerified",
+ "columnName": "manually_verified",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "nodeStatus",
+ "columnName": "node_status",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "lastTransport",
+ "columnName": "last_transport",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "num"
+ ]
+ },
+ "indices": [
+ {
+ "name": "index_nodes_last_heard",
+ "unique": false,
+ "columnNames": [
+ "last_heard"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_nodes_last_heard` ON `${TABLE_NAME}` (`last_heard`)"
+ },
+ {
+ "name": "index_nodes_short_name",
+ "unique": false,
+ "columnNames": [
+ "short_name"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_nodes_short_name` ON `${TABLE_NAME}` (`short_name`)"
+ },
+ {
+ "name": "index_nodes_long_name",
+ "unique": false,
+ "columnNames": [
+ "long_name"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_nodes_long_name` ON `${TABLE_NAME}` (`long_name`)"
+ },
+ {
+ "name": "index_nodes_hops_away",
+ "unique": false,
+ "columnNames": [
+ "hops_away"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_nodes_hops_away` ON `${TABLE_NAME}` (`hops_away`)"
+ },
+ {
+ "name": "index_nodes_is_favorite",
+ "unique": false,
+ "columnNames": [
+ "is_favorite"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_nodes_is_favorite` ON `${TABLE_NAME}` (`is_favorite`)"
+ },
+ {
+ "name": "index_nodes_last_heard_is_favorite",
+ "unique": false,
+ "columnNames": [
+ "last_heard",
+ "is_favorite"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_nodes_last_heard_is_favorite` ON `${TABLE_NAME}` (`last_heard`, `is_favorite`)"
+ },
+ {
+ "name": "index_nodes_public_key",
+ "unique": false,
+ "columnNames": [
+ "public_key"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_nodes_public_key` ON `${TABLE_NAME}` (`public_key`)"
+ }
+ ]
+ },
+ {
+ "tableName": "packet",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uuid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `myNodeNum` INTEGER NOT NULL DEFAULT 0, `port_num` INTEGER NOT NULL, `contact_key` TEXT NOT NULL, `received_time` INTEGER NOT NULL, `read` INTEGER NOT NULL DEFAULT 1, `data` TEXT NOT NULL, `packet_id` INTEGER NOT NULL DEFAULT 0, `routing_error` INTEGER NOT NULL DEFAULT -1, `snr` REAL NOT NULL DEFAULT 0, `rssi` INTEGER NOT NULL DEFAULT 0, `hopsAway` INTEGER NOT NULL DEFAULT -1, `sfpp_hash` BLOB, `filtered` INTEGER NOT NULL DEFAULT 0, `message_text` TEXT NOT NULL DEFAULT '')",
+ "fields": [
+ {
+ "fieldPath": "uuid",
+ "columnName": "uuid",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "myNodeNum",
+ "columnName": "myNodeNum",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "port_num",
+ "columnName": "port_num",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "contact_key",
+ "columnName": "contact_key",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "received_time",
+ "columnName": "received_time",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "read",
+ "columnName": "read",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "1"
+ },
+ {
+ "fieldPath": "data",
+ "columnName": "data",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "packetId",
+ "columnName": "packet_id",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "routingError",
+ "columnName": "routing_error",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "-1"
+ },
+ {
+ "fieldPath": "snr",
+ "columnName": "snr",
+ "affinity": "REAL",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "rssi",
+ "columnName": "rssi",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "hopsAway",
+ "columnName": "hopsAway",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "-1"
+ },
+ {
+ "fieldPath": "sfpp_hash",
+ "columnName": "sfpp_hash",
+ "affinity": "BLOB"
+ },
+ {
+ "fieldPath": "filtered",
+ "columnName": "filtered",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "messageText",
+ "columnName": "message_text",
+ "affinity": "TEXT",
+ "notNull": true,
+ "defaultValue": "''"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "uuid"
+ ]
+ },
+ "indices": [
+ {
+ "name": "index_packet_myNodeNum",
+ "unique": false,
+ "columnNames": [
+ "myNodeNum"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_packet_myNodeNum` ON `${TABLE_NAME}` (`myNodeNum`)"
+ },
+ {
+ "name": "index_packet_port_num",
+ "unique": false,
+ "columnNames": [
+ "port_num"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_packet_port_num` ON `${TABLE_NAME}` (`port_num`)"
+ },
+ {
+ "name": "index_packet_contact_key",
+ "unique": false,
+ "columnNames": [
+ "contact_key"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_packet_contact_key` ON `${TABLE_NAME}` (`contact_key`)"
+ },
+ {
+ "name": "index_packet_contact_key_port_num_received_time",
+ "unique": false,
+ "columnNames": [
+ "contact_key",
+ "port_num",
+ "received_time"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_packet_contact_key_port_num_received_time` ON `${TABLE_NAME}` (`contact_key`, `port_num`, `received_time`)"
+ },
+ {
+ "name": "index_packet_packet_id",
+ "unique": false,
+ "columnNames": [
+ "packet_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_packet_packet_id` ON `${TABLE_NAME}` (`packet_id`)"
+ },
+ {
+ "name": "index_packet_received_time",
+ "unique": false,
+ "columnNames": [
+ "received_time"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_packet_received_time` ON `${TABLE_NAME}` (`received_time`)"
+ },
+ {
+ "name": "index_packet_filtered",
+ "unique": false,
+ "columnNames": [
+ "filtered"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_packet_filtered` ON `${TABLE_NAME}` (`filtered`)"
+ },
+ {
+ "name": "index_packet_read",
+ "unique": false,
+ "columnNames": [
+ "read"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_packet_read` ON `${TABLE_NAME}` (`read`)"
+ }
+ ]
+ },
+ {
+ "tableName": "packet_fts",
+ "createSql": "CREATE VIRTUAL TABLE IF NOT EXISTS `${TABLE_NAME}` USING FTS5(`message_text`, tokenize=`unicode61`, content=`packet`)",
+ "fields": [
+ {
+ "fieldPath": "messageText",
+ "columnName": "message_text",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": []
+ },
+ "ftsVersion": "FTS5",
+ "ftsOptions": {
+ "tokenizer": "unicode61",
+ "tokenizerArgs": [],
+ "contentTable": "packet",
+ "languageIdColumnName": "",
+ "matchInfo": "FTS4",
+ "notIndexedColumns": [],
+ "prefixSizes": [],
+ "preferredOrder": "ASC",
+ "contentRowId": "",
+ "columnSize": true,
+ "detail": "FULL"
+ },
+ "contentSyncTriggers": [
+ "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_packet_fts_BEFORE_UPDATE BEFORE UPDATE ON `packet` BEGIN DELETE FROM `packet_fts` WHERE `rowid`=OLD.`rowid`; END",
+ "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_packet_fts_BEFORE_DELETE BEFORE DELETE ON `packet` BEGIN DELETE FROM `packet_fts` WHERE `rowid`=OLD.`rowid`; END",
+ "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_packet_fts_AFTER_UPDATE AFTER UPDATE ON `packet` BEGIN INSERT INTO `packet_fts`(`rowid`, `message_text`) VALUES (NEW.`rowid`, NEW.`message_text`); END",
+ "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_packet_fts_AFTER_INSERT AFTER INSERT ON `packet` BEGIN INSERT INTO `packet_fts`(`rowid`, `message_text`) VALUES (NEW.`rowid`, NEW.`message_text`); END"
+ ]
+ },
+ {
+ "tableName": "contact_settings",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`contact_key` TEXT NOT NULL, `muteUntil` INTEGER NOT NULL, `last_read_message_uuid` INTEGER, `last_read_message_timestamp` INTEGER, `filtering_disabled` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`contact_key`))",
+ "fields": [
+ {
+ "fieldPath": "contact_key",
+ "columnName": "contact_key",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "muteUntil",
+ "columnName": "muteUntil",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "lastReadMessageUuid",
+ "columnName": "last_read_message_uuid",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "lastReadMessageTimestamp",
+ "columnName": "last_read_message_timestamp",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "filteringDisabled",
+ "columnName": "filtering_disabled",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "contact_key"
+ ]
+ }
+ },
+ {
+ "tableName": "log",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uuid` TEXT NOT NULL, `type` TEXT NOT NULL, `received_date` INTEGER NOT NULL, `message` TEXT NOT NULL, `from_num` INTEGER NOT NULL DEFAULT 0, `port_num` INTEGER NOT NULL DEFAULT 0, `from_radio` BLOB NOT NULL DEFAULT x'', PRIMARY KEY(`uuid`))",
+ "fields": [
+ {
+ "fieldPath": "uuid",
+ "columnName": "uuid",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "message_type",
+ "columnName": "type",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "received_date",
+ "columnName": "received_date",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "raw_message",
+ "columnName": "message",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "fromNum",
+ "columnName": "from_num",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "portNum",
+ "columnName": "port_num",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "fromRadio",
+ "columnName": "from_radio",
+ "affinity": "BLOB",
+ "notNull": true,
+ "defaultValue": "x''"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "uuid"
+ ]
+ },
+ "indices": [
+ {
+ "name": "index_log_from_num",
+ "unique": false,
+ "columnNames": [
+ "from_num"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_log_from_num` ON `${TABLE_NAME}` (`from_num`)"
+ },
+ {
+ "name": "index_log_port_num",
+ "unique": false,
+ "columnNames": [
+ "port_num"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_log_port_num` ON `${TABLE_NAME}` (`port_num`)"
+ }
+ ]
+ },
+ {
+ "tableName": "quick_chat",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uuid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `message` TEXT NOT NULL, `mode` TEXT NOT NULL, `position` INTEGER NOT NULL)",
+ "fields": [
+ {
+ "fieldPath": "uuid",
+ "columnName": "uuid",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "message",
+ "columnName": "message",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "mode",
+ "columnName": "mode",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "position",
+ "columnName": "position",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "uuid"
+ ]
+ }
+ },
+ {
+ "tableName": "reactions",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`myNodeNum` INTEGER NOT NULL DEFAULT 0, `reply_id` INTEGER NOT NULL, `user_id` TEXT NOT NULL, `emoji` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, `snr` REAL NOT NULL DEFAULT 0, `rssi` INTEGER NOT NULL DEFAULT 0, `hopsAway` INTEGER NOT NULL DEFAULT -1, `packet_id` INTEGER NOT NULL DEFAULT 0, `status` INTEGER NOT NULL DEFAULT 0, `routing_error` INTEGER NOT NULL DEFAULT 0, `relays` INTEGER NOT NULL DEFAULT 0, `relay_node` INTEGER, `to` TEXT, `channel` INTEGER NOT NULL DEFAULT 0, `sfpp_hash` BLOB, PRIMARY KEY(`myNodeNum`, `reply_id`, `user_id`, `emoji`))",
+ "fields": [
+ {
+ "fieldPath": "myNodeNum",
+ "columnName": "myNodeNum",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "replyId",
+ "columnName": "reply_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "userId",
+ "columnName": "user_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "emoji",
+ "columnName": "emoji",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "timestamp",
+ "columnName": "timestamp",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "snr",
+ "columnName": "snr",
+ "affinity": "REAL",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "rssi",
+ "columnName": "rssi",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "hopsAway",
+ "columnName": "hopsAway",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "-1"
+ },
+ {
+ "fieldPath": "packetId",
+ "columnName": "packet_id",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "status",
+ "columnName": "status",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "routingError",
+ "columnName": "routing_error",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "relays",
+ "columnName": "relays",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "relayNode",
+ "columnName": "relay_node",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "to",
+ "columnName": "to",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "channel",
+ "columnName": "channel",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "sfpp_hash",
+ "columnName": "sfpp_hash",
+ "affinity": "BLOB"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "myNodeNum",
+ "reply_id",
+ "user_id",
+ "emoji"
+ ]
+ },
+ "indices": [
+ {
+ "name": "index_reactions_reply_id",
+ "unique": false,
+ "columnNames": [
+ "reply_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_reactions_reply_id` ON `${TABLE_NAME}` (`reply_id`)"
+ },
+ {
+ "name": "index_reactions_packet_id",
+ "unique": false,
+ "columnNames": [
+ "packet_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_reactions_packet_id` ON `${TABLE_NAME}` (`packet_id`)"
+ }
+ ]
+ },
+ {
+ "tableName": "metadata",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`num` INTEGER NOT NULL, `proto` BLOB NOT NULL, `timestamp` INTEGER NOT NULL, PRIMARY KEY(`num`))",
+ "fields": [
+ {
+ "fieldPath": "num",
+ "columnName": "num",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "proto",
+ "columnName": "proto",
+ "affinity": "BLOB",
+ "notNull": true
+ },
+ {
+ "fieldPath": "timestamp",
+ "columnName": "timestamp",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "num"
+ ]
+ },
+ "indices": [
+ {
+ "name": "index_metadata_num",
+ "unique": false,
+ "columnNames": [
+ "num"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_metadata_num` ON `${TABLE_NAME}` (`num`)"
+ }
+ ]
+ },
+ {
+ "tableName": "device_hardware",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`actively_supported` INTEGER NOT NULL, `architecture` TEXT NOT NULL, `display_name` TEXT NOT NULL, `has_ink_hud` INTEGER, `has_mui` INTEGER, `hwModel` INTEGER NOT NULL, `hw_model_slug` TEXT NOT NULL, `images` TEXT, `last_updated` INTEGER NOT NULL, `partition_scheme` TEXT, `platformio_target` TEXT NOT NULL, `requires_dfu` INTEGER, `support_level` INTEGER, `tags` TEXT, PRIMARY KEY(`platformio_target`))",
+ "fields": [
+ {
+ "fieldPath": "activelySupported",
+ "columnName": "actively_supported",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "architecture",
+ "columnName": "architecture",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "displayName",
+ "columnName": "display_name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "hasInkHud",
+ "columnName": "has_ink_hud",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "hasMui",
+ "columnName": "has_mui",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "hwModel",
+ "columnName": "hwModel",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "hwModelSlug",
+ "columnName": "hw_model_slug",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "images",
+ "columnName": "images",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "lastUpdated",
+ "columnName": "last_updated",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "partitionScheme",
+ "columnName": "partition_scheme",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "platformioTarget",
+ "columnName": "platformio_target",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "requiresDfu",
+ "columnName": "requires_dfu",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "supportLevel",
+ "columnName": "support_level",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "tags",
+ "columnName": "tags",
+ "affinity": "TEXT"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "platformio_target"
+ ]
+ }
+ },
+ {
+ "tableName": "device_link",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`short_code` TEXT NOT NULL, `link_description` TEXT, `is_vendor` INTEGER NOT NULL, `regions` TEXT, `targets` TEXT, PRIMARY KEY(`short_code`))",
+ "fields": [
+ {
+ "fieldPath": "shortCode",
+ "columnName": "short_code",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "linkDescription",
+ "columnName": "link_description",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "isVendor",
+ "columnName": "is_vendor",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "regions",
+ "columnName": "regions",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "targets",
+ "columnName": "targets",
+ "affinity": "TEXT"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "short_code"
+ ]
+ }
+ },
+ {
+ "tableName": "firmware_release",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `page_url` TEXT NOT NULL, `release_notes` TEXT NOT NULL, `title` TEXT NOT NULL, `zip_url` TEXT NOT NULL, `last_updated` INTEGER NOT NULL, `release_type` TEXT NOT NULL, PRIMARY KEY(`id`))",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "pageUrl",
+ "columnName": "page_url",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "releaseNotes",
+ "columnName": "release_notes",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "title",
+ "columnName": "title",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "zipUrl",
+ "columnName": "zip_url",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "lastUpdated",
+ "columnName": "last_updated",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "releaseType",
+ "columnName": "release_type",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "id"
+ ]
+ }
+ },
+ {
+ "tableName": "traceroute_node_position",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`log_uuid` TEXT NOT NULL, `request_id` INTEGER NOT NULL, `node_num` INTEGER NOT NULL, `position` BLOB NOT NULL, PRIMARY KEY(`log_uuid`, `node_num`), FOREIGN KEY(`log_uuid`) REFERENCES `log`(`uuid`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "logUuid",
+ "columnName": "log_uuid",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "requestId",
+ "columnName": "request_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "nodeNum",
+ "columnName": "node_num",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "position",
+ "columnName": "position",
+ "affinity": "BLOB",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "log_uuid",
+ "node_num"
+ ]
+ },
+ "indices": [
+ {
+ "name": "index_traceroute_node_position_log_uuid",
+ "unique": false,
+ "columnNames": [
+ "log_uuid"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_traceroute_node_position_log_uuid` ON `${TABLE_NAME}` (`log_uuid`)"
+ },
+ {
+ "name": "index_traceroute_node_position_request_id",
+ "unique": false,
+ "columnNames": [
+ "request_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_traceroute_node_position_request_id` ON `${TABLE_NAME}` (`request_id`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "log",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "log_uuid"
+ ],
+ "referencedColumns": [
+ "uuid"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "discovery_session",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `timestamp` INTEGER NOT NULL, `presets_scanned` TEXT NOT NULL, `home_preset` TEXT NOT NULL, `total_unique_nodes` INTEGER NOT NULL DEFAULT 0, `avg_channel_utilization` REAL NOT NULL DEFAULT 0.0, `total_messages` INTEGER NOT NULL DEFAULT 0, `total_sensor_packets` INTEGER NOT NULL DEFAULT 0, `furthest_node_distance` REAL NOT NULL DEFAULT 0.0, `completion_status` TEXT NOT NULL DEFAULT 'complete', `ai_summary` TEXT, `user_latitude` REAL NOT NULL DEFAULT 0.0, `user_longitude` REAL NOT NULL DEFAULT 0.0, `total_dwell_seconds` INTEGER NOT NULL DEFAULT 0)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "timestamp",
+ "columnName": "timestamp",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "presetsScanned",
+ "columnName": "presets_scanned",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "homePreset",
+ "columnName": "home_preset",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "totalUniqueNodes",
+ "columnName": "total_unique_nodes",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "avgChannelUtilization",
+ "columnName": "avg_channel_utilization",
+ "affinity": "REAL",
+ "notNull": true,
+ "defaultValue": "0.0"
+ },
+ {
+ "fieldPath": "totalMessages",
+ "columnName": "total_messages",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "totalSensorPackets",
+ "columnName": "total_sensor_packets",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "furthestNodeDistance",
+ "columnName": "furthest_node_distance",
+ "affinity": "REAL",
+ "notNull": true,
+ "defaultValue": "0.0"
+ },
+ {
+ "fieldPath": "completionStatus",
+ "columnName": "completion_status",
+ "affinity": "TEXT",
+ "notNull": true,
+ "defaultValue": "'complete'"
+ },
+ {
+ "fieldPath": "aiSummary",
+ "columnName": "ai_summary",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "userLatitude",
+ "columnName": "user_latitude",
+ "affinity": "REAL",
+ "notNull": true,
+ "defaultValue": "0.0"
+ },
+ {
+ "fieldPath": "userLongitude",
+ "columnName": "user_longitude",
+ "affinity": "REAL",
+ "notNull": true,
+ "defaultValue": "0.0"
+ },
+ {
+ "fieldPath": "totalDwellSeconds",
+ "columnName": "total_dwell_seconds",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ }
+ },
+ {
+ "tableName": "discovery_preset_result",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `session_id` INTEGER NOT NULL, `preset_name` TEXT NOT NULL, `dwell_duration_seconds` INTEGER NOT NULL DEFAULT 0, `unique_nodes` INTEGER NOT NULL DEFAULT 0, `direct_neighbor_count` INTEGER NOT NULL DEFAULT 0, `mesh_neighbor_count` INTEGER NOT NULL DEFAULT 0, `infrastructure_node_count` INTEGER NOT NULL DEFAULT 0, `message_count` INTEGER NOT NULL DEFAULT 0, `sensor_packet_count` INTEGER NOT NULL DEFAULT 0, `avg_channel_utilization` REAL NOT NULL DEFAULT 0.0, `avg_airtime_rate` REAL NOT NULL DEFAULT 0.0, `packet_success_rate` REAL NOT NULL DEFAULT 0.0, `packet_failure_rate` REAL NOT NULL DEFAULT 0.0, `ai_summary` TEXT, `num_packets_tx` INTEGER NOT NULL DEFAULT 0, `num_packets_rx` INTEGER NOT NULL DEFAULT 0, `num_packets_rx_bad` INTEGER NOT NULL DEFAULT 0, `num_rx_dupe` INTEGER NOT NULL DEFAULT 0, `num_tx_relay` INTEGER NOT NULL DEFAULT 0, `num_tx_relay_canceled` INTEGER NOT NULL DEFAULT 0, `num_online_nodes` INTEGER NOT NULL DEFAULT 0, `num_total_nodes` INTEGER NOT NULL DEFAULT 0, `uptime_seconds` INTEGER NOT NULL DEFAULT 0, FOREIGN KEY(`session_id`) REFERENCES `discovery_session`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "sessionId",
+ "columnName": "session_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "presetName",
+ "columnName": "preset_name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "dwellDurationSeconds",
+ "columnName": "dwell_duration_seconds",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "uniqueNodes",
+ "columnName": "unique_nodes",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "directNeighborCount",
+ "columnName": "direct_neighbor_count",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "meshNeighborCount",
+ "columnName": "mesh_neighbor_count",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "infrastructureNodeCount",
+ "columnName": "infrastructure_node_count",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "messageCount",
+ "columnName": "message_count",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "sensorPacketCount",
+ "columnName": "sensor_packet_count",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "avgChannelUtilization",
+ "columnName": "avg_channel_utilization",
+ "affinity": "REAL",
+ "notNull": true,
+ "defaultValue": "0.0"
+ },
+ {
+ "fieldPath": "avgAirtimeRate",
+ "columnName": "avg_airtime_rate",
+ "affinity": "REAL",
+ "notNull": true,
+ "defaultValue": "0.0"
+ },
+ {
+ "fieldPath": "packetSuccessRate",
+ "columnName": "packet_success_rate",
+ "affinity": "REAL",
+ "notNull": true,
+ "defaultValue": "0.0"
+ },
+ {
+ "fieldPath": "packetFailureRate",
+ "columnName": "packet_failure_rate",
+ "affinity": "REAL",
+ "notNull": true,
+ "defaultValue": "0.0"
+ },
+ {
+ "fieldPath": "aiSummary",
+ "columnName": "ai_summary",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "numPacketsTx",
+ "columnName": "num_packets_tx",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "numPacketsRx",
+ "columnName": "num_packets_rx",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "numPacketsRxBad",
+ "columnName": "num_packets_rx_bad",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "numRxDupe",
+ "columnName": "num_rx_dupe",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "numTxRelay",
+ "columnName": "num_tx_relay",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "numTxRelayCanceled",
+ "columnName": "num_tx_relay_canceled",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "numOnlineNodes",
+ "columnName": "num_online_nodes",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "numTotalNodes",
+ "columnName": "num_total_nodes",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "uptimeSeconds",
+ "columnName": "uptime_seconds",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [
+ {
+ "name": "index_discovery_preset_result_session_id",
+ "unique": false,
+ "columnNames": [
+ "session_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_discovery_preset_result_session_id` ON `${TABLE_NAME}` (`session_id`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "discovery_session",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "session_id"
+ ],
+ "referencedColumns": [
+ "id"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "discovered_node",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `preset_result_id` INTEGER NOT NULL, `node_num` INTEGER NOT NULL, `short_name` TEXT, `long_name` TEXT, `neighbor_type` TEXT NOT NULL DEFAULT 'direct', `latitude` REAL, `longitude` REAL, `distance_from_user` REAL, `hop_count` INTEGER NOT NULL DEFAULT 0, `snr` REAL NOT NULL DEFAULT 0, `rssi` INTEGER NOT NULL DEFAULT 0, `message_count` INTEGER NOT NULL DEFAULT 0, `sensor_packet_count` INTEGER NOT NULL DEFAULT 0, `is_infrastructure` INTEGER NOT NULL DEFAULT 0, FOREIGN KEY(`preset_result_id`) REFERENCES `discovery_preset_result`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "presetResultId",
+ "columnName": "preset_result_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "nodeNum",
+ "columnName": "node_num",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "shortName",
+ "columnName": "short_name",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "longName",
+ "columnName": "long_name",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "neighborType",
+ "columnName": "neighbor_type",
+ "affinity": "TEXT",
+ "notNull": true,
+ "defaultValue": "'direct'"
+ },
+ {
+ "fieldPath": "latitude",
+ "columnName": "latitude",
+ "affinity": "REAL"
+ },
+ {
+ "fieldPath": "longitude",
+ "columnName": "longitude",
+ "affinity": "REAL"
+ },
+ {
+ "fieldPath": "distanceFromUser",
+ "columnName": "distance_from_user",
+ "affinity": "REAL"
+ },
+ {
+ "fieldPath": "hopCount",
+ "columnName": "hop_count",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "snr",
+ "columnName": "snr",
+ "affinity": "REAL",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "rssi",
+ "columnName": "rssi",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "messageCount",
+ "columnName": "message_count",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "sensorPacketCount",
+ "columnName": "sensor_packet_count",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "isInfrastructure",
+ "columnName": "is_infrastructure",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [
+ {
+ "name": "index_discovered_node_preset_result_id",
+ "unique": false,
+ "columnNames": [
+ "preset_result_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_discovered_node_preset_result_id` ON `${TABLE_NAME}` (`preset_result_id`)"
+ },
+ {
+ "name": "index_discovered_node_node_num",
+ "unique": false,
+ "columnNames": [
+ "node_num"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_discovered_node_node_num` ON `${TABLE_NAME}` (`node_num`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "discovery_preset_result",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "preset_result_id"
+ ],
+ "referencedColumns": [
+ "id"
+ ]
+ }
+ ]
+ }
+ ],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'c58b8f2f228a2a98ef9faf320b388373')"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/core/database/src/commonMain/kotlin/org/meshtastic/core/database/MeshtasticDatabase.kt b/core/database/src/commonMain/kotlin/org/meshtastic/core/database/MeshtasticDatabase.kt
index 8b8d470ceb..bea88198d9 100644
--- a/core/database/src/commonMain/kotlin/org/meshtastic/core/database/MeshtasticDatabase.kt
+++ b/core/database/src/commonMain/kotlin/org/meshtastic/core/database/MeshtasticDatabase.kt
@@ -111,8 +111,9 @@ import org.meshtastic.core.database.entity.TracerouteNodePositionEntity
AutoMigration(from = 39, to = 40),
AutoMigration(from = 40, to = 41),
AutoMigration(from = 41, to = 42),
+ AutoMigration(from = 42, to = 43, spec = AutoMigration42to43::class),
],
- version = 42,
+ version = 43,
exportSchema = true,
)
@androidx.room3.ConstructedBy(MeshtasticDatabaseConstructor::class)
@@ -160,3 +161,7 @@ class AutoMigration33to34 : AutoMigrationSpec
@DeleteColumn(tableName = "packet", columnName = "retry_count")
@DeleteColumn(tableName = "reactions", columnName = "retry_count")
class AutoMigration34to35 : AutoMigrationSpec
+
+/** Device links moved from the bundled `urls.json` to the resolved API; `original_url` is no longer stored. */
+@DeleteColumn(tableName = "device_link", columnName = "original_url")
+class AutoMigration42to43 : AutoMigrationSpec
diff --git a/core/database/src/commonMain/kotlin/org/meshtastic/core/database/entity/DeviceLinkEntity.kt b/core/database/src/commonMain/kotlin/org/meshtastic/core/database/entity/DeviceLinkEntity.kt
index 91a9dde6bf..a5d56e2ff5 100644
--- a/core/database/src/commonMain/kotlin/org/meshtastic/core/database/entity/DeviceLinkEntity.kt
+++ b/core/database/src/commonMain/kotlin/org/meshtastic/core/database/entity/DeviceLinkEntity.kt
@@ -22,29 +22,29 @@ import androidx.room3.PrimaryKey
import kotlinx.serialization.Serializable
import org.meshtastic.core.model.DeviceLink
-/** A msh.to short-link, upserted from the bundled `urls.json` during the device-hardware refresh cycle. */
+/** A msh.to device link, cached from the Meshtastic API (`/resource/deviceLinks`) during the refresh cycle. */
@Serializable
@Entity(tableName = "device_link")
data class DeviceLinkEntity(
@PrimaryKey @ColumnInfo(name = "short_code") val shortCode: String,
- @ColumnInfo(name = "original_url") val originalUrl: String,
@ColumnInfo(name = "link_description") val linkDescription: String? = null,
@ColumnInfo(name = "is_vendor") val isVendor: Boolean = false,
val regions: List<String>? = null,
+ val targets: List<String>? = null,
)
fun DeviceLink.asEntity() = DeviceLinkEntity(
shortCode = shortCode,
- originalUrl = originalUrl,
linkDescription = description,
isVendor = isVendor,
regions = regions,
+ targets = targets,
)
fun DeviceLinkEntity.asExternalModel() = DeviceLink(
shortCode = shortCode,
- originalUrl = originalUrl,
description = linkDescription,
isVendor = isVendor,
regions = regions,
+ targets = targets,
)
diff --git a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/DeviceLink.kt b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/DeviceLink.kt
index 20b9197a34..696ff4c628 100644
--- a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/DeviceLink.kt
+++ b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/DeviceLink.kt
@@ -19,23 +19,23 @@ package org.meshtastic.core.model
import kotlinx.serialization.Serializable
/**
- * A msh.to short-link associated with a piece of hardware. Imported from the bundled `urls.json` (sourced from the
- * meshtastic/msh.to repo). Every link resolves through the msh.to redirect service.
+ * A msh.to device link resolved by the Meshtastic API (`/resource/deviceLinks`) and cached locally. Every link routes
+ * through the msh.to redirect service.
*
* @param shortCode the msh.to short code, e.g. `rak_wismeshtag`, `rokland-heltec-v3`.
- * @param originalUrl the destination URL recorded in `urls.json` (informational; the app links to msh.to).
* @param description human-readable label shown to the user.
- * @param isVendor true when [shortCode] is itself a known device `platformioTarget` (the primary vendor link).
- * @param regions marketplace shipping regions (ISO 3166-1 alpha-2). `null` = vendor/variant (not region-filtered);
- * empty = worldwide marketplace; non-empty = limited to the listed countries.
+ * @param isVendor true for a first-party vendor link (shown more prominently than region-filtered marketplace links).
+ * @param regions marketplace shipping regions (ISO 3166-1 alpha-2). `null` = not region-filtered (vendor/worldwide);
+ * non-empty = limited to the listed countries.
+ * @param targets device `platformioTarget`s this link is attached to; used to match a link to the device on screen.
*/
@Serializable
data class DeviceLink(
val shortCode: String,
- val originalUrl: String,
val description: String? = null,
val isVendor: Boolean = false,
val regions: List<String>? = null,
+ val targets: List<String>? = null,
) {
/** The user-facing link, routed through the msh.to redirect service. */
val url: String
diff --git a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/MshToLinks.kt b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/MshToLinks.kt
deleted file mode 100644
index 241a88d4b6..0000000000
--- a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/MshToLinks.kt
+++ /dev/null
@@ -1,41 +0,0 @@
-/*
- * Copyright (c) 2026 Meshtastic LLC
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <https://www.gnu.org/licenses/>.
- */
-package org.meshtastic.core.model
-
-import kotlinx.serialization.SerialName
-import kotlinx.serialization.Serializable
-
-/** Root of the bundled `urls.json` file (imported as-is from the meshtastic/msh.to repo). */
-@Serializable data class MshToUrlsFile(@SerialName("Routes") val routes: List<MshToRoute> = emptyList())
-
-/** A single short-code route in `urls.json`. */
-@Serializable
-data class MshToRoute(
- @SerialName("ShortCode") val shortCode: String,
- @SerialName("OriginalUrl") val originalUrl: String,
- @SerialName("Description") val description: String? = null,
-)
-
-/**
- * Marketplace metadata from the app-maintained `marketplaces.json`. Keyed by marketplace identifier (e.g. `rokland`,
- * `aliexpress`).
- *
- * @param regions ISO 3166-1 alpha-2 shipping regions; empty = worldwide.
- * @param match how the marketplace identifier appears in a short code: `"prefix"` (e.g. `rokland-heltec-v3`) or
- * `"suffix"` (e.g. `heltec-v3_aliexpress`).
- */
-@Serializable data class MshToMarketplace(val regions: List<String> = emptyList(), val match: String)
diff --git a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/NetworkDeviceLink.kt b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/NetworkDeviceLink.kt
new file mode 100644
index 0000000000..e22bb2d3d9
--- /dev/null
+++ b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/NetworkDeviceLink.kt
@@ -0,0 +1,80 @@
+/*
+ * Copyright (c) 2026 Meshtastic LLC
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+package org.meshtastic.core.model
+
+import kotlinx.serialization.ExperimentalSerializationApi
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.json.JsonIgnoreUnknownKeys
+
+/**
+ * Response envelope of `GET /resource/deviceLinks` on the Meshtastic API. The server resolves meshtastic/msh.to's
+ * catalog into fully-classified links (type + targets + regions), so the client only stores and filters them — no
+ * client-side matching heuristic.
+ */
+@Serializable
+@OptIn(ExperimentalSerializationApi::class)
+@JsonIgnoreUnknownKeys
+data class NetworkDeviceLinksResponse(
+ val version: Int = 1,
+ val generatedAt: String? = null,
+ val source: String? = null,
+ val links: List<NetworkDeviceLink> = emptyList(),
+)
+
+/**
+ * A single resolved device link from the Meshtastic API.
+ *
+ * @param shortCode msh.to short code, e.g. `rokland-t-deck-plus`.
+ * @param url the user-facing `https://msh.to/<shortCode>` link (the retailer destination is intentionally not exposed).
+ * @param description human-readable label.
+ * @param type authoritative classification: [TYPE_INTERNAL], [TYPE_VENDOR], or [TYPE_MARKETPLACE].
+ * @param targets device `platformioTarget`s this link is attached to; `null` = untriaged, empty = device-agnostic.
+ * @param hwModels `hwModel` ints derived from [targets] server-side (parallel list).
+ * @param marketplace retailer key (e.g. `rokland`) for marketplace links, else `null`.
+ * @param regions ISO 3166-1 alpha-2 shipping regions; `null` = worldwide (no region filter).
+ */
+@Serializable
+@OptIn(ExperimentalSerializationApi::class)
+@JsonIgnoreUnknownKeys
+data class NetworkDeviceLink(
+ val shortCode: String = "",
+ val url: String = "",
+ val description: String? = null,
+ val type: String = TYPE_INTERNAL,
+ val targets: List<String>? = null,
+ val hwModels: List<Int>? = null,
+ val marketplace: String? = null,
+ val regions: List<String>? = null,
+) {
+ companion object {
+ const val TYPE_INTERNAL = "internal"
+ const val TYPE_VENDOR = "vendor"
+ const val TYPE_MARKETPLACE = "marketplace"
+ }
+}
+
+/**
+ * Pure mapping to the cached domain model. Callers are expected to drop [TYPE_INTERNAL] links (GitHub, YouTube, …),
+ * which never belong to a device's purchase section.
+ */
+fun NetworkDeviceLink.toDeviceLink(): DeviceLink = DeviceLink(
+ shortCode = shortCode,
+ description = description,
+ isVendor = type == NetworkDeviceLink.TYPE_VENDOR,
+ regions = regions,
+ targets = targets,
+)
diff --git a/core/network/src/commonMain/kotlin/org/meshtastic/core/network/DeviceLinksRemoteDataSource.kt b/core/network/src/commonMain/kotlin/org/meshtastic/core/network/DeviceLinksRemoteDataSource.kt
new file mode 100644
index 0000000000..dab46d2f92
--- /dev/null
+++ b/core/network/src/commonMain/kotlin/org/meshtastic/core/network/DeviceLinksRemoteDataSource.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright (c) 2026 Meshtastic LLC
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+package org.meshtastic.core.network
+
+import kotlinx.coroutines.withContext
+import org.koin.core.annotation.Single
+import org.meshtastic.core.di.CoroutineDispatchers
+import org.meshtastic.core.model.NetworkDeviceLink
+import org.meshtastic.core.network.service.ApiService
+
+@Single
+class DeviceLinksRemoteDataSource(private val apiService: ApiService, private val dispatchers: CoroutineDispatchers) {
+ suspend fun getDeviceLinks(): List<NetworkDeviceLink> =
+ withContext(dispatchers.io) { apiService.getDeviceLinks().links }
+}
diff --git a/core/network/src/commonMain/kotlin/org/meshtastic/core/network/service/ApiService.kt b/core/network/src/commonMain/kotlin/org/meshtastic/core/network/service/ApiService.kt
index ec51e56026..94c7697a9a 100644
--- a/core/network/src/commonMain/kotlin/org/meshtastic/core/network/service/ApiService.kt
+++ b/core/network/src/commonMain/kotlin/org/meshtastic/core/network/service/ApiService.kt
@@ -21,6 +21,7 @@ import io.ktor.client.call.body
import io.ktor.client.request.get
import org.koin.core.annotation.Single
import org.meshtastic.core.model.NetworkDeviceHardware
+import org.meshtastic.core.model.NetworkDeviceLinksResponse
import org.meshtastic.core.model.NetworkFirmwareReleases
/** Client for the Meshtastic public API (device hardware catalog and firmware releases). */
@@ -28,6 +29,9 @@ interface ApiService {
/** Fetches the device hardware catalog from the Meshtastic API. */
suspend fun getDeviceHardware(): List<NetworkDeviceHardware>
+ /** Fetches the resolved device-links catalog (msh.to purchase links) from the Meshtastic API. */
+ suspend fun getDeviceLinks(): NetworkDeviceLinksResponse
+
/** Fetches the list of available firmware releases from the Meshtastic API. */
suspend fun getFirmwareReleases(): NetworkFirmwareReleases
}
@@ -44,5 +48,7 @@ interface ApiService {
class ApiServiceImpl(private val client: HttpClient) : ApiService {
override suspend fun getDeviceHardware(): List<NetworkDeviceHardware> = client.get("resource/deviceHardware").body()
+ override suspend fun getDeviceLinks(): NetworkDeviceLinksResponse = client.get("resource/deviceLinks").body()
+
override suspend fun getFirmwareReleases(): NetworkFirmwareReleases = client.get("github/firmware/list").body()
}
diff --git a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/DeviceLinkRepository.kt b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/DeviceLinkRepository.kt
index 7bfe3a2210..c3f9cace9d 100644
--- a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/DeviceLinkRepository.kt
+++ b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/DeviceLinkRepository.kt
@@ -21,23 +21,22 @@ import org.meshtastic.core.common.util.currentRegionCode
import org.meshtastic.core.model.DeviceLink
/**
- * Provides msh.to device links imported from the bundled `urls.json`. Mirrors the Meshtastic-Apple device-links
- * feature: vendor, product-variant, and region-filtered marketplace links shown on the device hardware detail view,
- * plus a full directory.
+ * Provides msh.to device links resolved by the Meshtastic API (`/resource/deviceLinks`) and cached locally. Vendor and
+ * region-filtered marketplace links are shown on the device hardware detail view, plus a full directory.
*/
interface DeviceLinkRepository {
- /** Seeds the link table from the bundled JSON if it is empty (covers fresh install, data clear, radio switch). */
+ /** Seeds the link table from the bundled snapshot if it is empty (fresh install, data clear, radio switch). */
suspend fun ensureImported()
- /** Re-imports the bundled JSON: upserts all links, recomputes `isVendor`, and prunes orphaned short codes. */
+ /** Refreshes links from the API: upserts the resolved catalog and prunes short codes that no longer exist. */
suspend fun reconcile()
/**
- * Links for a device's [platformioTarget], region-filtered and sorted with vendor/variant links first. Returns an
+ * Links attached to a device's [platformioTarget], region-filtered and sorted with vendor links first. Returns an
* empty list when no links match.
*/
suspend fun getLinksForTarget(platformioTarget: String, regionCode: String = currentRegionCode()): List<DeviceLink>
- /** All imported links, sorted by short code — backs the Settings "Device Links" directory. */
+ /** All cached links, sorted by short code — backs the Settings "Device Links" directory. */
fun observeAllLinks(): Flow<List<DeviceLink>>
}
diff --git a/desktopApp/src/main/kotlin/org/meshtastic/desktop/di/DesktopKoinModule.kt b/desktopApp/src/main/kotlin/org/meshtastic/desktop/di/DesktopKoinModule.kt
index 9c69c9867d..c47a1d4b04 100644
--- a/desktopApp/src/main/kotlin/org/meshtastic/desktop/di/DesktopKoinModule.kt
+++ b/desktopApp/src/main/kotlin/org/meshtastic/desktop/di/DesktopKoinModule.kt
@@ -36,12 +36,11 @@ import org.koin.core.qualifier.named
import org.koin.dsl.module
import org.meshtastic.core.data.datasource.BootloaderOtaQuirksJsonDataSource
import org.meshtastic.core.data.datasource.DeviceHardwareJsonDataSource
+import org.meshtastic.core.data.datasource.DeviceLinksJsonDataSource
import org.meshtastic.core.data.datasource.FirmwareReleaseJsonDataSource
-import org.meshtastic.core.data.datasource.MshToLinksJsonDataSource
import org.meshtastic.core.model.BootloaderOtaQuirk
-import org.meshtastic.core.model.MshToMarketplace
-import org.meshtastic.core.model.MshToRoute
import org.meshtastic.core.model.NetworkDeviceHardware
+import org.meshtastic.core.model.NetworkDeviceLink
import org.meshtastic.core.model.NetworkFirmwareReleases
import org.meshtastic.core.network.HttpClientDefaults
import org.meshtastic.core.network.KermitHttpLogger
@@ -273,11 +272,9 @@ private fun desktopPlatformStubsModule() = module {
}
}
- single<MshToLinksJsonDataSource> {
- object : MshToLinksJsonDataSource {
- override fun loadRoutes(): List<MshToRoute> = emptyList()
-
- override fun loadMarketplaces(): Map<String, MshToMarketplace> = emptyMap()
+ single<DeviceLinksJsonDataSource> {
+ object : DeviceLinksJsonDataSource {
+ override fun loadDeviceLinksFromJsonAsset(): List<NetworkDeviceLink> = emptyList()
}
}
}
diff --git a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/NodeDetailComponentPreviews.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/NodeDetailComponentPreviews.kt
index 70b79be5be..b090a25995 100644
--- a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/NodeDetailComponentPreviews.kt
+++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/NodeDetailComponentPreviews.kt
@@ -191,21 +191,21 @@ private fun DeviceLinksSectionPreview() {
listOf(
org.meshtastic.core.model.DeviceLink(
shortCode = "heltec-v3",
- originalUrl = "https://heltec.org",
description = "Heltec V3",
isVendor = true,
+ targets = listOf("heltec-v3"),
),
org.meshtastic.core.model.DeviceLink(
shortCode = "rokland-heltec-v3",
- originalUrl = "https://rokland.com",
description = "Rokland",
regions = listOf("US"),
+ targets = listOf("heltec-v3"),
),
org.meshtastic.core.model.DeviceLink(
shortCode = "heltec-v3_aliexpress",
- originalUrl = "https://aliexpress.com",
description = "AliExpress",
regions = emptyList(),
+ targets = listOf("heltec-v3"),
),
)
AppTheme { Surface { DeviceLinksSection(links = links) } }
Served by rngit 1.5.0 - Generated in 0.3s